package analytics import ( "reflect" "testing" "cloudipvalidator/internal/db" ) // set stores the four checks of an address: https to a.test and b.test, ssh // from the two sites. func (f *fixture) set(reg int64, validator string, a, b, ssh1, ssh2 bool) { f.check(reg, eg, "https", "https://a.test", a, validator, "", false) f.check(reg, eg, "https", "https://b.test", b, validator, "", false) f.check(reg, s1, "ssh", "ip", ssh1, validator, "dial tcp: i/o timeout", false) f.check(reg, s2, "ssh", "ip", ssh2, validator, "dial tcp: i/o timeout", false) } // comparedRuns builds the older and the newer run: // // 10.0.0.1 pass -> pass, nothing changed // 10.0.0.2 partial -> pass: https recovered on both targets, another validator // 10.0.0.3 pass -> partial: ssh fails from rxmsk // 10.0.0.20 partial -> partial: another https target fails, same indicators // 10.0.0.10 only in the old run (fail) // 10.0.0.14 only in the old run (pass), cancelled in the new one // 10.0.0.11 only in the new run (partial, https fails everywhere) // 10.0.0.12 only in the new run (pass) // 10.0.0.13 only in the new run (pass), cancelled in the old one func comparedRuns(t *testing.T) (base, target *Analysis) { t.Helper() fa, fb := &fixture{}, &fixture{} fa.addr(1, "10.0.0.1", db.ResultPass, 4) fa.set(1, "vkiplab-v1", true, true, true, true) fa.addr(2, "10.0.0.2", db.ResultPartial, 4) fa.set(2, "vkiplab-v2", false, false, true, true) fa.addr(3, "10.0.0.3", db.ResultPass, 4) fa.set(3, "vkiplab-v3", true, true, true, true) fa.addr(4, "10.0.0.20", db.ResultPartial, 4) fa.set(4, "vkiplab-v4", false, true, true, true) fa.addr(5, "10.0.0.10", db.ResultFail, 4) fa.set(5, "vkiplab-v5", false, false, false, false) fa.addr(6, "10.0.0.13", db.ResultCancelled, 4) fa.set(6, "vkiplab-v6", true, true, true, true) fa.addr(7, "10.0.0.14", db.ResultPass, 4) fa.set(7, "vkiplab-v7", true, true, true, true) fb.addr(1, "10.0.0.1", db.ResultPass, 4) fb.set(1, "vkiplab-v1", true, true, true, true) fb.addr(2, "10.0.0.2", db.ResultPass, 4) fb.set(2, "vkiplab-v5", true, true, true, true) fb.addr(3, "10.0.0.3", db.ResultPartial, 4) fb.set(3, "vkiplab-v3", true, true, false, true) fb.addr(4, "10.0.0.20", db.ResultPartial, 4) fb.set(4, "vkiplab-v4", true, false, true, true) fb.addr(8, "10.0.0.11", db.ResultPartial, 4) fb.set(8, "vkiplab-v6", false, false, true, true) fb.addr(9, "10.0.0.12", db.ResultPass, 4) fb.set(9, "vkiplab-v6", true, true, true, true) fb.addr(10, "10.0.0.13", db.ResultPass, 4) fb.set(10, "vkiplab-v6", true, true, true, true) fb.addr(11, "10.0.0.14", db.ResultCancelled, 4) fb.set(11, "vkiplab-v7", true, true, true, true) return fa.compute(t, nil), fb.compute(t, nil) } func ips(l *CompareList) []string { out := []string{} for _, r := range l.Rows { out = append(out, r[0]) } return out } func mustList(t *testing.T, c *Comparison, group string, f CompareFilter) *CompareList { t.Helper() l, err := c.List(group, f) if err != nil { t.Fatalf("%s %+v: %v", group, f, err) } for _, r := range l.Rows { if len(r) != len(l.Columns) { t.Errorf("%s: row %v for columns %v", group, r, l.Columns) } } return l } func TestCompareGroupsAndIndicators(t *testing.T) { base, target := comparedRuns(t) c := Compare(base, target) if want := (CompareGroups{New: 3, Left: 2, Common: 4, Changed: 2, Same: 2}); c.Groups != want { t.Errorf("groups = %+v, want %+v", c.Groups, want) } // Every address is in exactly one of new, left, common; common = changed + same. if c.Groups.New+c.Groups.Common != target.Report.Summary.Addresses || c.Groups.Left+c.Groups.Common != base.Report.Summary.Addresses || c.Groups.Changed+c.Groups.Same != c.Groups.Common { t.Errorf("groups do not add up: %+v, runs %d and %d addresses", c.Groups, base.Report.Summary.Addresses, target.Report.Summary.Addresses) } if c.Cancelled != (CompareCancel{Base: 1, Target: 1}) { t.Errorf("cancelled = %+v", c.Cancelled) } if c.Runs.Base.Addresses != 6 || c.Runs.Target.Addresses != 7 || c.Runs.Base.ID != 7 { t.Errorf("runs = %+v", c.Runs) } // The count of each indicator in each run is the number of its card. sa, sb := base.Report.Summary, target.Report.Summary cards := map[string][2]int{ ListVerdictPass: {sa.Pass, sb.Pass}, ListVerdictPartial: {sa.Partial, sb.Partial}, ListVerdictFail: {sa.Fail, sb.Fail}, ListEgressHTTPSAny: {sa.EgressHTTPSAny, sb.EgressHTTPSAny}, ListEgressHTTPSAll: {sa.EgressHTTPSAll, sb.EgressHTTPSAll}, ListIngressSSHAny: {sa.IngressSSHAny, sb.IngressSSHAny}, ListIngressSSHAll: {sa.IngressSSHAll, sb.IngressSSHAll}, } if len(c.Indicators) != len(cards) { t.Fatalf("%d indicators", len(c.Indicators)) } for _, d := range c.Indicators { if want := cards[d.Key]; d.Base != want[0] || d.Target != want[1] { t.Errorf("%s: %d -> %d, summaries say %v", d.Key, d.Base, d.Target, want) } if d.Delta != d.Target-d.Base || d.Delta != d.New-d.Left+d.Entered-d.Exited { t.Errorf("%s: delta %d, new %d left %d entered %d exited %d", d.Key, d.Delta, d.New, d.Left, d.Entered, d.Exited) } } want := map[string]IndicatorDiff{ ListVerdictPass: {Base: 3, Target: 4, Delta: 1, New: 2, Left: 1, Entered: 1, Exited: 1}, ListVerdictPartial: {Base: 2, Target: 3, Delta: 1, New: 1, Entered: 1, Exited: 1}, ListVerdictFail: {Base: 1, Target: 0, Delta: -1, Left: 1}, ListEgressHTTPSAny: {Base: 3, Target: 2, Delta: -1, New: 1, Left: 1, Exited: 1}, ListEgressHTTPSAll: {Base: 2, Target: 1, Delta: -1, New: 1, Left: 1, Exited: 1}, ListIngressSSHAny: {Base: 1, Target: 1, Left: 1, Entered: 1}, ListIngressSSHAll: {Base: 1, Target: 0, Delta: -1, Left: 1}, } for _, d := range c.Indicators { w := want[d.Key] w.Key, w.Name = d.Key, d.Name if d != w || d.Name == "" { t.Errorf("%s = %+v, want %+v", d.Key, d, w) } } // Verdicts of the common addresses, and of the new and of the left ones. tr := c.Transitions if !reflect.DeepEqual(tr.Verdicts, []string{"pass", "partial", "fail"}) || !reflect.DeepEqual(tr.Matrix, [][]int{{1, 1, 0}, {1, 1, 0}, {0, 0, 0}}) || !reflect.DeepEqual(tr.New, []int{2, 1, 0}) || !reflect.DeepEqual(tr.Left, []int{1, 0, 1}) { t.Errorf("transitions = %+v", tr) } } func TestCompareLists(t *testing.T) { base, target := comparedRuns(t) c := Compare(base, target) // New and left: the state in the run the address is in; numeric order. l := mustList(t, c, GroupNew, CompareFilter{}) if want := []string{"Адрес", "Подсеть", "Вердикт", "Egress", "Ingress", "Индикаторы"}; !reflect.DeepEqual(l.Columns, want) { t.Errorf("new columns: %v", l.Columns) } if got := ips(l); !reflect.DeepEqual(got, []string{"10.0.0.11", "10.0.0.12", "10.0.0.13"}) { t.Errorf("new: %v", got) } if want := []string{"10.0.0.11", "10.0.0.0/24", "partial", "0 из 2", "2 из 2", "partial, Egress https: есть провалы, Egress https: все провалены"}; !reflect.DeepEqual(l.Rows[0], want) { t.Errorf("new row: %v", l.Rows[0]) } if got := ips(mustList(t, c, GroupNew, CompareFilter{Indicator: ListVerdictPass})); !reflect.DeepEqual(got, []string{"10.0.0.12", "10.0.0.13"}) { t.Errorf("new, pass: %v", got) } if got := ips(mustList(t, c, GroupNew, CompareFilter{Indicator: ListEgressHTTPSAll})); !reflect.DeepEqual(got, []string{"10.0.0.11"}) { t.Errorf("new, https all: %v", got) } l = mustList(t, c, GroupLeft, CompareFilter{}) if got := ips(l); !reflect.DeepEqual(got, []string{"10.0.0.10", "10.0.0.14"}) { t.Errorf("left: %v", got) } if want := "fail"; l.Rows[0][2] != want || l.Rows[0][5] != "fail, Egress https: есть провалы, Egress https: все провалены, Ingress ssh: есть провалы, Ingress ssh: все провалены" { t.Errorf("left row: %v", l.Rows[0]) } if got := ips(mustList(t, c, GroupLeft, CompareFilter{Indicator: ListVerdictPass})); !reflect.DeepEqual(got, []string{"10.0.0.14"}) { t.Errorf("left, pass: %v", got) } // Changed: what changed, step by step. l = mustList(t, c, GroupChanged, CompareFilter{}) if got := ips(l); !reflect.DeepEqual(got, []string{"10.0.0.2", "10.0.0.3"}) { t.Fatalf("changed: %v", got) } if l.Columns[2] != "Вердикт (A → B)" || l.Columns[5] != "Что изменилось" { t.Errorf("changed columns: %v", l.Columns) } if want := []string{"10.0.0.2", "10.0.0.0/24", "partial → pass", "0 из 2 → 2 из 2", "2 из 2 → 2 из 2", "вердикт partial → pass; вышел из: Egress https: есть провалы, Egress https: все провалены; https: провалены цели −a.test −b.test; валидатор v2 → v5"}; !reflect.DeepEqual(l.Rows[0], want) { t.Errorf("changed row 1: %v", l.Rows[0]) } if want := []string{"10.0.0.3", "10.0.0.0/24", "pass → partial", "2 из 2 → 2 из 2", "2 из 2 → 1 из 2", "вердикт pass → partial; вошёл в: Ingress ssh: есть провалы; ssh: площадки +rxmsk"}; !reflect.DeepEqual(l.Rows[1], want) { t.Errorf("changed row 2: %v", l.Rows[1]) } // A filter by indicator keeps the addresses that are in it in either run. if got := ips(mustList(t, c, GroupChanged, CompareFilter{Indicator: ListIngressSSHAny})); !reflect.DeepEqual(got, []string{"10.0.0.3"}) { t.Errorf("changed, ssh any: %v", got) } if got := ips(mustList(t, c, GroupChanged, CompareFilter{Indicator: ListEgressHTTPSAll})); !reflect.DeepEqual(got, []string{"10.0.0.2"}) { t.Errorf("changed, https all: %v", got) } // Same: another failed target does not change the indicators; numeric order (20 after 1). l = mustList(t, c, GroupSame, CompareFilter{}) if got := ips(l); !reflect.DeepEqual(got, []string{"10.0.0.1", "10.0.0.20"}) { t.Fatalf("same: %v", got) } if want := []string{"10.0.0.20", "10.0.0.0/24", "partial → partial", "1 из 2 → 1 из 2", "2 из 2 → 2 из 2", "без изменений"}; !reflect.DeepEqual(l.Rows[1], want) { t.Errorf("same row: %v", l.Rows[1]) } if got := ips(mustList(t, c, GroupSame, CompareFilter{Indicator: ListEgressHTTPSAny})); !reflect.DeepEqual(got, []string{"10.0.0.20"}) { t.Errorf("same, https any: %v", got) } // Common is changed + same, in address order. l = mustList(t, c, GroupCommon, CompareFilter{}) if got := ips(l); !reflect.DeepEqual(got, []string{"10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.20"}) || l.Rows[3][5] != "без изменений" { t.Errorf("common: %v", l.Rows) } // Entered and exited need an indicator. for _, x := range []struct { group, ind string want []string }{ {GroupEntered, ListVerdictPass, []string{"10.0.0.2"}}, {GroupExited, ListVerdictPass, []string{"10.0.0.3"}}, {GroupEntered, ListVerdictPartial, []string{"10.0.0.3"}}, {GroupEntered, ListIngressSSHAny, []string{"10.0.0.3"}}, {GroupExited, ListEgressHTTPSAll, []string{"10.0.0.2"}}, {GroupExited, ListEgressHTTPSAny, []string{"10.0.0.2"}}, {GroupEntered, ListEgressHTTPSAny, []string{}}, {GroupEntered, ListVerdictFail, []string{}}, } { got := ips(mustList(t, c, x.group, CompareFilter{Indicator: x.ind})) if !reflect.DeepEqual(got, x.want) { t.Errorf("%s %s: %v, want %v", x.group, x.ind, got, x.want) } } // A cell of the transition matrix: the verdict in the old and in the new run. for _, x := range []struct { group, from, to string want []string }{ {GroupCommon, "partial", "pass", []string{"10.0.0.2"}}, {GroupCommon, "pass", "partial", []string{"10.0.0.3"}}, {GroupCommon, "pass", "pass", []string{"10.0.0.1"}}, {GroupCommon, "partial", "partial", []string{"10.0.0.20"}}, {GroupSame, "partial", "partial", []string{"10.0.0.20"}}, {GroupChanged, "pass", "pass", []string{}}, {GroupCommon, "fail", "pass", []string{}}, } { got := ips(mustList(t, c, x.group, CompareFilter{From: x.from, To: x.to})) if !reflect.DeepEqual(got, x.want) { t.Errorf("%s %s -> %s: %v, want %v", x.group, x.from, x.to, got, x.want) } } // The cells of the matrix and the lists add up. n := 0 for _, from := range verdicts { for _, to := range verdicts { n += len(mustList(t, c, GroupCommon, CompareFilter{From: from, To: to}).Rows) } } if n != c.Groups.Common { t.Errorf("matrix cells hold %d addresses, %d are common", n, c.Groups.Common) } // The size of every list equals its number in the report. for _, d := range c.Indicators { for group, want := range map[string]int{GroupNew: d.New, GroupLeft: d.Left, GroupEntered: d.Entered, GroupExited: d.Exited} { if got := len(mustList(t, c, group, CompareFilter{Indicator: d.Key}).Rows); got != want { t.Errorf("%s %s: %d rows, report says %d", group, d.Key, got, want) } } } for group, want := range map[string]int{GroupNew: c.Groups.New, GroupLeft: c.Groups.Left, GroupCommon: c.Groups.Common, GroupChanged: c.Groups.Changed, GroupSame: c.Groups.Same} { if got := len(mustList(t, c, group, CompareFilter{}).Rows); got != want { t.Errorf("%s: %d rows, report says %d", group, got, want) } } } func TestCompareListErrors(t *testing.T) { base, target := comparedRuns(t) c := Compare(base, target) for name, x := range map[string]struct { group string f CompareFilter }{ "unknown group": {"nonsense", CompareFilter{}}, "unknown indicator": {GroupNew, CompareFilter{Indicator: "nonsense"}}, "entered needs one": {GroupEntered, CompareFilter{}}, "exited needs one": {GroupExited, CompareFilter{}}, "from without to": {GroupCommon, CompareFilter{From: "pass"}}, "to without from": {GroupCommon, CompareFilter{To: "pass"}}, "unknown verdict": {GroupCommon, CompareFilter{From: "pass", To: "cancelled"}}, "verdicts of new addrs": {GroupNew, CompareFilter{From: "pass", To: "pass"}}, "verdicts of left addrs": {GroupLeft, CompareFilter{From: "pass", To: "pass"}}, } { l, err := c.List(x.group, x.f) if _, ok := err.(ErrUnknownList); !ok || l != nil { t.Errorf("%s: %v %v", name, l, err) } } } // A run compared with itself: nothing new, nothing left, nothing changed, and // the empty lists are empty tables, not nil. func TestCompareWithItself(t *testing.T) { base, _ := comparedRuns(t) c := Compare(base, base) if want := (CompareGroups{Common: 6, Same: 6}); c.Groups != want { t.Errorf("groups = %+v", c.Groups) } for _, d := range c.Indicators { if d.Delta != 0 || d.New != 0 || d.Left != 0 || d.Entered != 0 || d.Exited != 0 || d.Base != d.Target { t.Errorf("%+v", d) } } for _, group := range []string{GroupNew, GroupLeft, GroupChanged} { if l := mustList(t, c, group, CompareFilter{}); l.Rows == nil || len(l.Rows) != 0 { t.Errorf("%s: %#v", group, l.Rows) } } if !reflect.DeepEqual(c.Transitions.Matrix, [][]int{{3, 0, 0}, {0, 2, 0}, {0, 0, 1}}) { t.Errorf("matrix = %v", c.Transitions.Matrix) } }