package engine import ( "context" "net/netip" "strings" "testing" "steward/internal/auth" "steward/internal/model" "steward/internal/store" ) func newEngine(t *testing.T) *Engine { t.Helper() st, err := store.Open(":memory:") if err != nil { t.Fatal(err) } e, err := New(context.Background(), st, nil) if err != nil { t.Fatal(err) } return e } var admin = &auth.Principal{User: &model.User{ID: 1, Username: "admin", Admin: true}, Method: "session"} func addOffice(t *testing.T, e *Engine) int64 { t.Helper() var id int64 res, err := e.Apply(context.Background(), admin, "", Options{Via: "cli"}, func(st *model.State, newID func() int64) error { p, _ := model.ParsePool("10.0.10.100-10.0.10.199") id = newID() st.Subnets = append(st.Subnets, &model.Subnet{ID: id, Name: "office", Interface: "eth0", Prefix: netip.MustParsePrefix("10.0.10.0/24"), Pools: []model.Pool{p}, Options: []model.OptionValue{{Code: 6, Value: "10.0.10.2,10.0.10.3"}}}) return nil }) if err != nil || !res.Applied || res.HistoryID == 0 { t.Fatalf("apply: %+v %v", res, err) } return id } func TestApplyNormalizesAndRecords(t *testing.T) { e := newEngine(t) id := addOffice(t, e) s := e.State().SubnetByID(id) if s.Options[0].Value != "10.0.10.2, 10.0.10.3" || s.Options[0].Name != "domain-name-servers" { t.Errorf("options not normalized: %+v", s.Options) } h, _ := e.Store().ListHistory(context.Background(), 10, 0) if len(h) != 1 || h[0].Description != "create subnet office (10.0.10.0/24)" || h[0].Via != "cli" || h[0].Username != "admin" { t.Errorf("history %+v", h[0]) } // Nothing changed: nothing recorded. res, err := e.Apply(context.Background(), admin, "", Options{}, func(st *model.State, _ func() int64) error { return nil }) if err != nil || res.Applied || len(res.Changes) != 0 { t.Errorf("empty change: %+v %v", res, err) } // Reloading from the database gives the same state. e2, err := New(context.Background(), e.Store(), nil) if err != nil { t.Fatal(err) } if d := Diff(e.State(), e2.State()); len(d) != 0 { t.Errorf("reloaded state differs: %+v", d) } } func TestValidationRefuses(t *testing.T) { e := newEngine(t) addOffice(t, e) _, err := e.Apply(context.Background(), admin, "", Options{}, func(st *model.State, newID func() int64) error { st.Subnets = append(st.Subnets, &model.Subnet{ID: newID(), Interface: "eth1", Prefix: netip.MustParsePrefix("10.0.0.0/16")}) return nil }) ee, ok := AsError(err) if !ok || ee.Kind != KindValidation || !strings.Contains(ee.Message, "overlaps subnet office") { t.Fatalf("got %v", err) } if len(e.State().Subnets) != 1 { t.Error("refused change was kept") } } func TestDryRun(t *testing.T) { e := newEngine(t) res, err := e.Apply(context.Background(), admin, "", Options{DryRun: true}, func(st *model.State, newID func() int64) error { st.Subnets = append(st.Subnets, &model.Subnet{ID: newID(), Interface: "eth0", Prefix: netip.MustParsePrefix("10.0.10.0/24")}) return nil }) if err != nil || res.Applied || len(res.Changes) != 1 || len(res.Warnings) != 1 || res.Warnings[0].Code != "subnet_no_pools" { t.Fatalf("dry run: %+v %v", res, err) } if len(e.State().Subnets) != 0 { t.Error("dry run changed the state") } } func TestRevert(t *testing.T) { ctx := context.Background() e := newEngine(t) var got *model.State e.OnChange = func(st *model.State) { got = st } id := addOffice(t, e) res, err := e.Apply(ctx, admin, "longer leases", Options{}, func(st *model.State, _ func() int64) error { st.SubnetByID(id).LeaseTime = 86400 return nil }) if err != nil { t.Fatal(err) } if got == nil || got.SubnetByID(id).LeaseTime != 86400 { t.Error("OnChange didn't get the new state") } // Undoing the creation conflicts: the subnet changed since. if _, err := e.Revert(ctx, admin, 1, Options{}); err == nil || !strings.Contains(err.Error(), "has changed since entry 1") { t.Fatalf("revert of an older entry: %v", err) } if _, err := e.Revert(ctx, admin, res.HistoryID, Options{}); err != nil { t.Fatal(err) } if e.State().SubnetByID(id).LeaseTime != 0 { t.Error("revert didn't restore the lease time") } if _, err := e.Revert(ctx, admin, res.HistoryID, Options{}); err == nil || !strings.Contains(err.Error(), "already undone") { t.Errorf("second revert: %v", err) } if _, err := e.Revert(ctx, admin, 1, Options{}); err != nil { t.Fatalf("revert of the creation: %v", err) } if len(e.State().Subnets) != 0 { t.Error("subnet still there") } h, _ := e.Store().GetHistory(ctx, 1) if h.RevertedBy == 0 { t.Error("entry 1 not marked undone") } }