2026-10-03 17:59:52 +03:00
|
|
|
package db
|
|
|
|
|
|
|
|
|
|
import (
|
2026-10-06 14:23:48 +03:00
|
|
|
"errors"
|
2026-10-03 17:59:52 +03:00
|
|
|
"reflect"
|
2026-10-06 14:23:48 +03:00
|
|
|
"sort"
|
2026-10-03 17:59:52 +03:00
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestCheckLevelAndFamily(t *testing.T) {
|
|
|
|
|
for source, want := range map[string]string{
|
|
|
|
|
"egress": LevelEgress, "inbound-site-1": LevelIngress, "inbound-site-12": LevelIngress,
|
|
|
|
|
"": "", "other": "",
|
|
|
|
|
} {
|
|
|
|
|
if got := CheckLevel(source); got != want {
|
|
|
|
|
t.Errorf("CheckLevel(%q) = %q, want %q", source, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for ct, want := range map[string]string{
|
|
|
|
|
"https": "https", "icmp": "icmp", "ssh": "ssh",
|
|
|
|
|
"tcp-22": "tcp", "tcp-443": "tcp", "tls-443": "tls", "dns": "dns", "-x": "-x",
|
|
|
|
|
} {
|
|
|
|
|
if got := CheckFamily(ct); got != want {
|
|
|
|
|
t.Errorf("CheckFamily(%q) = %q, want %q", ct, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addCheck records one check for the address's current queue row.
|
|
|
|
|
func addCheck(t *testing.T, d *DB, addr, source, checkType, target string, success bool) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
ip, err := d.GetIPByAddress(ctx, addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("get %s: %v", addr, err)
|
|
|
|
|
}
|
|
|
|
|
if err := d.UpsertCheck(ctx, Check{
|
|
|
|
|
IPID: ip.ID, IPAddress: addr, AttemptNumber: ip.AttemptNumber,
|
|
|
|
|
Source: source, CheckType: checkType, Target: target,
|
|
|
|
|
Success: success, CheckedAt: Now(),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("upsert check: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegistryLevelsGroupByTypeAndLevel(t *testing.T) {
|
|
|
|
|
d, ctx := newTestDB(t)
|
|
|
|
|
if _, err := d.SubmitIPs(ctx, []string{"1.2.3.4"}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
a := "1.2.3.4"
|
|
|
|
|
// Egress: https 2 of 3, icmp 1 of 1.
|
|
|
|
|
addCheck(t, d, a, SourceEgress, "https", "https://a.test", true)
|
|
|
|
|
addCheck(t, d, a, SourceEgress, "https", "https://b.test", true)
|
|
|
|
|
addCheck(t, d, a, SourceEgress, "https", "https://c.test", false)
|
|
|
|
|
addCheck(t, d, a, SourceEgress, "icmp", "a.test", true)
|
|
|
|
|
// Ingress from two sites: tcp-22 and tcp-443 are one family; tls, ssh,
|
|
|
|
|
// icmp and a type unknown today ("dns") are listed on their own.
|
|
|
|
|
for site := 1; site <= 2; site++ {
|
|
|
|
|
src := InboundSource(site)
|
|
|
|
|
addCheck(t, d, a, src, "tcp-22", a, true)
|
|
|
|
|
addCheck(t, d, a, src, "tcp-443", a, site == 1)
|
|
|
|
|
addCheck(t, d, a, src, "tls-443", a, true)
|
|
|
|
|
addCheck(t, d, a, src, "ssh", a, false)
|
|
|
|
|
addCheck(t, d, a, src, "icmp", a, true)
|
|
|
|
|
addCheck(t, d, a, src, "dns", a, true)
|
|
|
|
|
}
|
|
|
|
|
// A source that is neither egress nor an inbound site is not counted.
|
|
|
|
|
addCheck(t, d, a, "manual", "https", "x", true)
|
|
|
|
|
|
|
|
|
|
s, err := d.GetRegistryByAddress(ctx, a)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
wantEgress := LevelResult{Total: 4, OK: 3, ByType: []TypeStat{
|
|
|
|
|
{"https", 3, 2}, {"icmp", 1, 1},
|
|
|
|
|
}}
|
|
|
|
|
wantIngress := LevelResult{Total: 12, OK: 9, ByType: []TypeStat{
|
|
|
|
|
{"dns", 2, 2}, {"icmp", 2, 2}, {"ssh", 2, 0}, {"tcp", 4, 3}, {"tls", 2, 2},
|
|
|
|
|
}}
|
|
|
|
|
if !reflect.DeepEqual(s.Egress, wantEgress) {
|
|
|
|
|
t.Errorf("egress = %+v, want %+v", s.Egress, wantEgress)
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(s.Ingress, wantIngress) {
|
|
|
|
|
t.Errorf("ingress = %+v, want %+v", s.Ingress, wantIngress)
|
|
|
|
|
}
|
|
|
|
|
if s.LastCycleID != 1 {
|
|
|
|
|
t.Errorf("LastCycleID = %d, want 1", s.LastCycleID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegistryLevelsNoChecksAreZero(t *testing.T) {
|
|
|
|
|
d, ctx := newTestDB(t)
|
|
|
|
|
if _, err := d.SubmitIPs(ctx, []string{"1.2.3.4"}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
s, err := d.GetRegistryByAddress(ctx, "1.2.3.4")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if s.LastCycleID != 0 || s.Egress.Total != 0 || s.Ingress.Total != 0 || len(s.Egress.ByType) != 0 {
|
|
|
|
|
t.Fatalf("expected empty levels, got %+v", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The counts follow the newest cycle, also after the queue row is deleted,
|
|
|
|
|
// and one grouped query serves several addresses without mixing them up.
|
|
|
|
|
func TestRegistryLevelsLatestCycleAndManyAddresses(t *testing.T) {
|
|
|
|
|
d, ctx := newTestDB(t)
|
|
|
|
|
if _, err := d.SubmitIPs(ctx, []string{"1.1.1.1", "2.2.2.2"}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
addCheck(t, d, "1.1.1.1", SourceEgress, "https", "t1", false)
|
|
|
|
|
addCheck(t, d, "2.2.2.2", SourceEgress, "https", "t1", true)
|
|
|
|
|
addCheck(t, d, "2.2.2.2", InboundSource(1), "tcp-22", "2.2.2.2", true)
|
|
|
|
|
|
|
|
|
|
// New cycle for 1.1.1.1: delete and submit again.
|
|
|
|
|
ip, err := d.GetIPByAddress(ctx, "1.1.1.1")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := d.DeleteIP(ctx, ip.ID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := d.SubmitIPs(ctx, []string{"1.1.1.1"}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
addCheck(t, d, "1.1.1.1", SourceEgress, "icmp", "t2", true)
|
|
|
|
|
addCheck(t, d, "1.1.1.1", SourceEgress, "https", "t2", true)
|
|
|
|
|
|
|
|
|
|
all, err := d.ListRegistry(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
got := map[string]RegistrySummary{}
|
|
|
|
|
for _, s := range all {
|
|
|
|
|
got[s.IPAddress] = s
|
|
|
|
|
}
|
|
|
|
|
a := got["1.1.1.1"]
|
|
|
|
|
if a.LastCycleID != 2 || a.Egress.Total != 2 || a.Egress.OK != 2 || a.Ingress.Total != 0 {
|
|
|
|
|
t.Errorf("1.1.1.1: %+v", a)
|
|
|
|
|
}
|
|
|
|
|
b := got["2.2.2.2"]
|
|
|
|
|
if b.LastCycleID != 1 || b.Egress.Total != 1 || b.Ingress.Total != 1 || b.Ingress.OK != 1 {
|
|
|
|
|
t.Errorf("2.2.2.2: %+v", b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Page and single lookups agree with the full list.
|
|
|
|
|
page, _, err := d.ListRegistryPage(ctx, RegistryFilter{}, 10, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, s := range page {
|
|
|
|
|
if !reflect.DeepEqual(s.Egress, got[s.IPAddress].Egress) || !reflect.DeepEqual(s.Ingress, got[s.IPAddress].Ingress) {
|
|
|
|
|
t.Errorf("page differs from list for %s", s.IPAddress)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete the queue row of 2.2.2.2: the counts stay (history outlives it).
|
|
|
|
|
ip2, _ := d.GetIPByAddress(ctx, "2.2.2.2")
|
|
|
|
|
if err := d.DeleteIP(ctx, ip2.ID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
s, err := d.GetRegistryByAddress(ctx, "2.2.2.2")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if s.Egress.Total != 1 || s.Ingress.Total != 1 {
|
|
|
|
|
t.Errorf("after delete: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-06 14:23:48 +03:00
|
|
|
|
|
|
|
|
// TestRegistryPageSlice covers the run / level / protocol filters of
|
|
|
|
|
// ListRegistryPage together with the status and the subnet. Run 1 holds three
|
|
|
|
|
// addresses (cycle 1), run 2 only a re-check of .1 (cycle 2) whose result
|
|
|
|
|
// differs from run 1.
|
|
|
|
|
func TestRegistryPageSlice(t *testing.T) {
|
|
|
|
|
d, ctx := newTestDB(t)
|
|
|
|
|
const a1, a2, a3 = "10.0.0.1", "10.0.0.2", "10.0.1.1"
|
|
|
|
|
submit(t, d, RunManual, a1, a2, a3)
|
|
|
|
|
for _, c := range []struct {
|
|
|
|
|
addr, source, typ string
|
|
|
|
|
ok bool
|
|
|
|
|
}{
|
|
|
|
|
{a1, SourceEgress, "https", true}, {a1, SourceEgress, "icmp", true},
|
|
|
|
|
{a1, InboundSource(1), "tcp-22", true}, {a1, InboundSource(1), "tcp-443", false}, {a1, InboundSource(1), "tls-443", false},
|
|
|
|
|
{a2, SourceEgress, "https", false},
|
|
|
|
|
{a2, InboundSource(1), "tcp-443", true}, {a2, InboundSource(1), "tls-443", true}, {a2, InboundSource(1), "ssh", true},
|
|
|
|
|
{a3, InboundSource(1), "icmp", true},
|
|
|
|
|
} {
|
|
|
|
|
addCheck(t, d, c.addr, c.source, c.typ, c.addr, c.ok)
|
|
|
|
|
}
|
|
|
|
|
finish(t, d, a1, ResultPartial, -1)
|
|
|
|
|
finish(t, d, a2, ResultPartial, -1)
|
|
|
|
|
finish(t, d, a3, ResultPass, -1)
|
|
|
|
|
run1 := runs(t, d)[0].ID
|
|
|
|
|
|
|
|
|
|
submit(t, d, RunManual, a1)
|
|
|
|
|
addCheck(t, d, a1, SourceEgress, "https", a1, false)
|
|
|
|
|
addCheck(t, d, a1, InboundSource(1), "tcp-443", a1, true)
|
|
|
|
|
addCheck(t, d, a1, InboundSource(1), "tls-443", a1, true)
|
|
|
|
|
finish(t, d, a1, ResultPartial, -1)
|
|
|
|
|
run2 := runs(t, d)[0].ID
|
|
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
f RegistryFilter
|
|
|
|
|
want []string
|
|
|
|
|
}{
|
|
|
|
|
{"no filter", RegistryFilter{}, []string{a1, a2, a3}},
|
|
|
|
|
{"run 1", RegistryFilter{RunID: run1}, []string{a1, a2, a3}},
|
|
|
|
|
{"run 2", RegistryFilter{RunID: run2}, []string{a1}},
|
|
|
|
|
{"unknown run", RegistryFilter{RunID: 999}, nil},
|
|
|
|
|
{"egress, newest cycle", RegistryFilter{Level: LevelEgress}, []string{a1, a2}},
|
|
|
|
|
{"ingress, newest cycle", RegistryFilter{Level: LevelIngress}, []string{a1, a2, a3}},
|
|
|
|
|
{"verdict of the run", RegistryFilter{RunID: run1, LastResult: ResultPartial}, []string{a1, a2}},
|
|
|
|
|
{"cancelled in the run", RegistryFilter{RunID: run1, LastResult: ResultCancelled}, nil},
|
|
|
|
|
{"tcp-22 and tcp-443 are tcp", RegistryFilter{RunID: run1, Family: "tcp"}, []string{a1, a2}},
|
|
|
|
|
{"tcp partial", RegistryFilter{RunID: run1, Family: "tcp", LastResult: ResultPartial}, []string{a1}},
|
|
|
|
|
{"tcp pass", RegistryFilter{RunID: run1, Family: "tcp", LastResult: ResultPass}, []string{a2}},
|
|
|
|
|
{"tcp fail", RegistryFilter{RunID: run1, Family: "tcp", LastResult: ResultFail}, nil},
|
|
|
|
|
{"tls fail in run 1", RegistryFilter{RunID: run1, Family: "tls", LastResult: ResultFail}, []string{a1}},
|
|
|
|
|
{"tls pass in run 2", RegistryFilter{RunID: run2, Family: "tls", LastResult: ResultPass}, []string{a1}},
|
|
|
|
|
{"tls on egress is empty", RegistryFilter{RunID: run1, Level: LevelEgress, Family: "tls"}, nil},
|
|
|
|
|
{"egress status is of egress checks", RegistryFilter{RunID: run1, Level: LevelEgress, LastResult: ResultPass}, []string{a1}},
|
|
|
|
|
{"egress fail", RegistryFilter{RunID: run1, Level: LevelEgress, LastResult: ResultFail}, []string{a2}},
|
|
|
|
|
{"cancelled has no scope", RegistryFilter{Level: LevelEgress, LastResult: ResultCancelled}, nil},
|
|
|
|
|
{"icmp on ingress", RegistryFilter{RunID: run1, Level: LevelIngress, Family: "icmp"}, []string{a3}},
|
|
|
|
|
{"subnet, run and level", RegistryFilter{RunID: run1, Level: LevelIngress, Subnet: "10.0.0.0/24"}, []string{a1, a2}},
|
|
|
|
|
{"subnet with no checks of the level", RegistryFilter{RunID: run1, Level: LevelEgress, Subnet: "10.0.1.0/24"}, nil},
|
|
|
|
|
{"query and level", RegistryFilter{Query: ".2", Level: LevelEgress}, []string{a2}},
|
|
|
|
|
} {
|
|
|
|
|
page, total, err := d.ListRegistryPage(ctx, tc.f, 50, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
|
|
|
}
|
|
|
|
|
var got []string
|
|
|
|
|
for _, s := range page {
|
|
|
|
|
got = append(got, s.IPAddress)
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(got)
|
|
|
|
|
if total != len(tc.want) || !reflect.DeepEqual(got, tc.want) {
|
|
|
|
|
t.Errorf("%s: total=%d got=%v, want %v", tc.name, total, got, tc.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The same address shows its own result in each run, narrowed to the scope.
|
|
|
|
|
for run, want := range map[int64]struct {
|
|
|
|
|
cycle int
|
|
|
|
|
result LevelResult
|
|
|
|
|
}{
|
|
|
|
|
run1: {1, LevelResult{Total: 1, OK: 0, ByType: []TypeStat{{"tls", 1, 0}}}},
|
|
|
|
|
run2: {2, LevelResult{Total: 1, OK: 1, ByType: []TypeStat{{"tls", 1, 1}}}},
|
|
|
|
|
} {
|
|
|
|
|
page, _, err := d.ListRegistryPage(ctx, RegistryFilter{RunID: run, Query: a1, Family: "tls"}, 10, 0)
|
|
|
|
|
if err != nil || len(page) != 1 {
|
|
|
|
|
t.Fatalf("run %d: %v %+v", run, err, page)
|
|
|
|
|
}
|
|
|
|
|
s := page[0]
|
|
|
|
|
if s.LastCycleID != want.cycle || s.LastResult != ResultPartial || s.Egress.Total != 0 || !reflect.DeepEqual(s.Ingress, want.result) {
|
|
|
|
|
t.Errorf("run %d: cycle=%d result=%q egress=%+v ingress=%+v", run, s.LastCycleID, s.LastResult, s.Egress, s.Ingress)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, f := range []RegistryFilter{{Level: "sideways"}, {Family: "dns"}} {
|
|
|
|
|
if _, _, err := d.ListRegistryPage(ctx, f, 10, 0); !errors.Is(err, ErrValidation) {
|
|
|
|
|
t.Errorf("%+v: %v", f, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|