package store import ( "context" "encoding/json" "errors" "path/filepath" "testing" "time" "housecarl/internal/model" ) func raw(v any) json.RawMessage { b, _ := json.Marshal(v) return b } func TestStateAndHistory(t *testing.T) { ctx := context.Background() s, err := Open(filepath.Join(t.TempDir(), "housecarl.db")) if err != nil { t.Fatal(err) } defer s.Close() st, err := s.LoadState(ctx) if err != nil { t.Fatal(err) } if st.Settings.InputPolicy != model.PolicyDrop || len(st.Zones) != 0 || !st.Settings.Builtins.Established { t.Fatalf("new database state: %+v", st) } ids, _ := s.AllocateIDs(ctx, 2) z := &model.Zone{ID: ids[0], Name: "wan", Interfaces: []string{"wan"}, External: true} r := &model.Rule{ID: ids[1], Chain: model.ChainInput, Position: 1, Action: model.ActionAccept, Match: model.Match{Services: []string{"ssh"}, Sources: []string{"10.0.10.0/24"}}} set := st.Settings set.ForwardPolicy = model.PolicyAccept e := &model.HistoryEntry{Time: time.Now(), Username: "admin", Via: "cli", Description: "add wan", Changes: []model.Change{ {Kind: model.KindZone, ID: z.ID, After: raw(z)}, {Kind: model.KindRule, ID: r.ID, After: raw(r)}, {Kind: model.KindSettings, Before: raw(st.Settings), After: raw(set)}, }} if err := s.SaveChanges(ctx, e.Changes, e); err != nil { t.Fatal(err) } st, _ = s.LoadState(ctx) if len(st.Zones) != 1 || !st.Zones[0].External || st.Zones[0].Interfaces[0] != "wan" { t.Errorf("zones %+v", st.Zones) } if len(st.Rules) != 1 || st.Rules[0].Services[0] != "ssh" || st.Settings.ForwardPolicy != model.PolicyAccept { t.Errorf("rules %+v settings %+v", st.Rules, st.Settings) } // Undo: the inverse changes, marked as a revert. var inv []model.Change for _, c := range e.Changes { inv = append(inv, c.Inverse()) } u := &model.HistoryEntry{Time: time.Now(), Username: "admin", Via: "web", Description: "undo", Changes: inv, RevertOf: e.ID} if err := s.SaveChanges(ctx, inv, u); err != nil { t.Fatal(err) } st, _ = s.LoadState(ctx) if len(st.Zones) != 0 || len(st.Rules) != 0 || st.Settings.ForwardPolicy != model.PolicyDrop { t.Errorf("after undo: %+v", st) } h, err := s.GetHistory(ctx, e.ID) if err != nil || h.RevertedBy != u.ID || len(h.Changes) != 3 { t.Errorf("history %+v %v", h, err) } list, _ := s.ListHistory(ctx, 10, 0) if len(list) != 2 || list[0].ID != u.ID { t.Errorf("history list %+v", list) } } func TestUsersAndAudit(t *testing.T) { ctx := context.Background() s, err := Open(":memory:") if err != nil { t.Fatal(err) } u := &model.User{Username: "admin", PasswordHash: "x", Admin: true} if err := s.CreateUser(ctx, u); err != nil { t.Fatal(err) } if err := s.CreateUser(ctx, &model.User{Username: "ADMIN"}); !errors.Is(err, ErrConflict) { t.Errorf("duplicate user: %v", err) } tok := &model.Token{UserID: u.ID, Name: "ci", PublicID: "p1", SecretHash: "h"} if err := s.CreateToken(ctx, tok); err != nil { t.Fatal(err) } got, err := s.GetTokenByPublicID(ctx, "p1") if err != nil || got.Username != "admin" || got.Grants != nil { t.Errorf("token %+v %v", got, err) } s.AddAudit(ctx, &model.AuditEvent{Username: "admin", Action: "auth.login", Result: "success"}) s.AddAudit(ctx, &model.AuditEvent{Username: "admin", Action: "set.add", Object: "guest 10.0.30.5", Result: "success"}) evs, _ := s.ListAudit(ctx, AuditFilter{Action: "set."}) if len(evs) != 1 || evs[0].Object != "guest 10.0.30.5" { t.Errorf("audit %+v", evs) } }