package database import ( "context" "testing" "time" ) // runStoreTests runs the test suite against a Store implementation. func runStoreTests(t *testing.T, newStore func(t *testing.T) Store) { tests := []struct { name string test func(t *testing.T, store Store) }{ {"Initialize", testInitialize}, {"Revisions", testRevisions}, {"Options", testOptions}, {"OptionsSearch", testOptionsSearch}, {"OptionChildren", testOptionChildren}, {"Declarations", testDeclarations}, {"Files", testFiles}, {"SchemaVersion", testSchemaVersion}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { store := newStore(t) defer store.Close() tt.test(t, store) }) } } func testInitialize(t *testing.T, store Store) { ctx := context.Background() // Initialize should work if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } // Initialize again should be idempotent if err := store.Initialize(ctx); err != nil { t.Fatalf("Second Initialize failed: %v", err) } } func testRevisions(t *testing.T, store Store) { ctx := context.Background() if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } // Create a revision rev := &Revision{ GitHash: "abc123def456", ChannelName: "nixos-unstable", CommitDate: time.Now().UTC().Truncate(time.Second), OptionCount: 0, } if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } if rev.ID == 0 { t.Error("Expected revision ID to be set") } // Get by git hash got, err := store.GetRevision(ctx, "abc123def456") if err != nil { t.Fatalf("GetRevision failed: %v", err) } if got == nil { t.Fatal("Expected revision, got nil") } if got.GitHash != rev.GitHash { t.Errorf("GitHash = %q, want %q", got.GitHash, rev.GitHash) } if got.ChannelName != rev.ChannelName { t.Errorf("ChannelName = %q, want %q", got.ChannelName, rev.ChannelName) } // Get by channel name got, err = store.GetRevisionByChannel(ctx, "nixos-unstable") if err != nil { t.Fatalf("GetRevisionByChannel failed: %v", err) } if got == nil { t.Fatal("Expected revision, got nil") } if got.ID != rev.ID { t.Errorf("ID = %d, want %d", got.ID, rev.ID) } // Get non-existent got, err = store.GetRevision(ctx, "nonexistent") if err != nil { t.Fatalf("GetRevision for nonexistent failed: %v", err) } if got != nil { t.Error("Expected nil for nonexistent revision") } // List revisions revs, err := store.ListRevisions(ctx) if err != nil { t.Fatalf("ListRevisions failed: %v", err) } if len(revs) != 1 { t.Errorf("Expected 1 revision, got %d", len(revs)) } // Update option count if err := store.UpdateRevisionOptionCount(ctx, rev.ID, 100); err != nil { t.Fatalf("UpdateRevisionOptionCount failed: %v", err) } got, _ = store.GetRevision(ctx, "abc123def456") if got.OptionCount != 100 { t.Errorf("OptionCount = %d, want 100", got.OptionCount) } // Delete revision if err := store.DeleteRevision(ctx, rev.ID); err != nil { t.Fatalf("DeleteRevision failed: %v", err) } revs, _ = store.ListRevisions(ctx) if len(revs) != 0 { t.Errorf("Expected 0 revisions after delete, got %d", len(revs)) } } func testOptions(t *testing.T, store Store) { ctx := context.Background() if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } // Create a revision first rev := &Revision{GitHash: "test123", ChannelName: "test"} if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } // Create an option opt := &Option{ RevisionID: rev.ID, Name: "services.nginx.enable", ParentPath: "services.nginx", Type: "boolean", DefaultValue: "false", Description: "Whether to enable nginx.", ReadOnly: false, } if err := store.CreateOption(ctx, opt); err != nil { t.Fatalf("CreateOption failed: %v", err) } if opt.ID == 0 { t.Error("Expected option ID to be set") } // Get option got, err := store.GetOption(ctx, rev.ID, "services.nginx.enable") if err != nil { t.Fatalf("GetOption failed: %v", err) } if got == nil { t.Fatal("Expected option, got nil") } if got.Name != opt.Name { t.Errorf("Name = %q, want %q", got.Name, opt.Name) } if got.Type != opt.Type { t.Errorf("Type = %q, want %q", got.Type, opt.Type) } if got.Description != opt.Description { t.Errorf("Description = %q, want %q", got.Description, opt.Description) } // Get non-existent option got, err = store.GetOption(ctx, rev.ID, "nonexistent") if err != nil { t.Fatalf("GetOption for nonexistent failed: %v", err) } if got != nil { t.Error("Expected nil for nonexistent option") } // Batch create options opts := []*Option{ {RevisionID: rev.ID, Name: "services.caddy.enable", ParentPath: "services.caddy", Type: "boolean"}, {RevisionID: rev.ID, Name: "services.caddy.config", ParentPath: "services.caddy", Type: "string"}, } if err := store.CreateOptionsBatch(ctx, opts); err != nil { t.Fatalf("CreateOptionsBatch failed: %v", err) } for _, o := range opts { if o.ID == 0 { t.Errorf("Expected option %q ID to be set", o.Name) } } } func testOptionsSearch(t *testing.T, store Store) { ctx := context.Background() if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } rev := &Revision{GitHash: "search123", ChannelName: "test"} if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } opts := []*Option{ {RevisionID: rev.ID, Name: "services.nginx.enable", ParentPath: "services.nginx", Type: "boolean", Description: "Enable the nginx web server"}, {RevisionID: rev.ID, Name: "services.nginx.package", ParentPath: "services.nginx", Type: "package", Description: "Nginx package to use"}, {RevisionID: rev.ID, Name: "services.caddy.enable", ParentPath: "services.caddy", Type: "boolean", Description: "Enable the caddy web server"}, {RevisionID: rev.ID, Name: "programs.git.enable", ParentPath: "programs.git", Type: "boolean", Description: "Enable git version control"}, } if err := store.CreateOptionsBatch(ctx, opts); err != nil { t.Fatalf("CreateOptionsBatch failed: %v", err) } // Search for "nginx" results, err := store.SearchOptions(ctx, rev.ID, "nginx", SearchFilters{}) if err != nil { t.Fatalf("SearchOptions failed: %v", err) } if len(results) < 1 { t.Errorf("Expected at least 1 result for 'nginx', got %d", len(results)) } // Search with namespace filter results, err = store.SearchOptions(ctx, rev.ID, "enable", SearchFilters{Namespace: "services"}) if err != nil { t.Fatalf("SearchOptions with namespace failed: %v", err) } for _, r := range results { if r.Name[:8] != "services" { t.Errorf("Result %q doesn't match namespace filter", r.Name) } } // Search with type filter results, err = store.SearchOptions(ctx, rev.ID, "nginx", SearchFilters{Type: "boolean"}) if err != nil { t.Fatalf("SearchOptions with type failed: %v", err) } for _, r := range results { if r.Type != "boolean" { t.Errorf("Result %q has type %q, expected boolean", r.Name, r.Type) } } // Search with limit results, err = store.SearchOptions(ctx, rev.ID, "enable", SearchFilters{Limit: 2}) if err != nil { t.Fatalf("SearchOptions with limit failed: %v", err) } if len(results) > 2 { t.Errorf("Expected at most 2 results, got %d", len(results)) } } func testOptionChildren(t *testing.T, store Store) { ctx := context.Background() if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } rev := &Revision{GitHash: "children123", ChannelName: "test"} if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } // Create a hierarchy of options opts := []*Option{ {RevisionID: rev.ID, Name: "services", ParentPath: "", Type: "attrsOf"}, {RevisionID: rev.ID, Name: "services.nginx", ParentPath: "services", Type: "submodule"}, {RevisionID: rev.ID, Name: "services.nginx.enable", ParentPath: "services.nginx", Type: "boolean"}, {RevisionID: rev.ID, Name: "services.nginx.package", ParentPath: "services.nginx", Type: "package"}, {RevisionID: rev.ID, Name: "services.caddy", ParentPath: "services", Type: "submodule"}, } if err := store.CreateOptionsBatch(ctx, opts); err != nil { t.Fatalf("CreateOptionsBatch failed: %v", err) } // Get children of root children, err := store.GetChildren(ctx, rev.ID, "") if err != nil { t.Fatalf("GetChildren root failed: %v", err) } if len(children) != 1 { t.Errorf("Expected 1 root child, got %d", len(children)) } if len(children) > 0 && children[0].Name != "services" { t.Errorf("Expected root child to be 'services', got %q", children[0].Name) } // Get children of services children, err = store.GetChildren(ctx, rev.ID, "services") if err != nil { t.Fatalf("GetChildren services failed: %v", err) } if len(children) != 2 { t.Errorf("Expected 2 children of services, got %d", len(children)) } // Get children of services.nginx children, err = store.GetChildren(ctx, rev.ID, "services.nginx") if err != nil { t.Fatalf("GetChildren services.nginx failed: %v", err) } if len(children) != 2 { t.Errorf("Expected 2 children of services.nginx, got %d", len(children)) } } func testDeclarations(t *testing.T, store Store) { ctx := context.Background() if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } rev := &Revision{GitHash: "decl123", ChannelName: "test"} if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } opt := &Option{ RevisionID: rev.ID, Name: "services.nginx.enable", ParentPath: "services.nginx", Type: "boolean", } if err := store.CreateOption(ctx, opt); err != nil { t.Fatalf("CreateOption failed: %v", err) } // Create declarations decl := &Declaration{ OptionID: opt.ID, FilePath: "nixos/modules/services/web-servers/nginx/default.nix", Line: 42, } if err := store.CreateDeclaration(ctx, decl); err != nil { t.Fatalf("CreateDeclaration failed: %v", err) } if decl.ID == 0 { t.Error("Expected declaration ID to be set") } // Get declarations decls, err := store.GetDeclarations(ctx, opt.ID) if err != nil { t.Fatalf("GetDeclarations failed: %v", err) } if len(decls) != 1 { t.Fatalf("Expected 1 declaration, got %d", len(decls)) } if decls[0].FilePath != decl.FilePath { t.Errorf("FilePath = %q, want %q", decls[0].FilePath, decl.FilePath) } if decls[0].Line != 42 { t.Errorf("Line = %d, want 42", decls[0].Line) } // Batch create batch := []*Declaration{ {OptionID: opt.ID, FilePath: "file1.nix", Line: 10}, {OptionID: opt.ID, FilePath: "file2.nix", Line: 20}, } if err := store.CreateDeclarationsBatch(ctx, batch); err != nil { t.Fatalf("CreateDeclarationsBatch failed: %v", err) } decls, _ = store.GetDeclarations(ctx, opt.ID) if len(decls) != 3 { t.Errorf("Expected 3 declarations after batch, got %d", len(decls)) } } func testFiles(t *testing.T, store Store) { ctx := context.Background() if err := store.Initialize(ctx); err != nil { t.Fatalf("Initialize failed: %v", err) } rev := &Revision{GitHash: "files123", ChannelName: "test"} if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } // Create a file file := &File{ RevisionID: rev.ID, FilePath: "nixos/modules/services/web-servers/nginx/default.nix", Extension: ".nix", Content: "{ config, lib, pkgs, ... }:\n\n# nginx module\n", } if err := store.CreateFile(ctx, file); err != nil { t.Fatalf("CreateFile failed: %v", err) } if file.ID == 0 { t.Error("Expected file ID to be set") } // Get file got, err := store.GetFile(ctx, rev.ID, "nixos/modules/services/web-servers/nginx/default.nix") if err != nil { t.Fatalf("GetFile failed: %v", err) } if got == nil { t.Fatal("Expected file, got nil") } if got.Content != file.Content { t.Errorf("Content mismatch") } if got.Extension != ".nix" { t.Errorf("Extension = %q, want .nix", got.Extension) } // Get non-existent file got, err = store.GetFile(ctx, rev.ID, "nonexistent.nix") if err != nil { t.Fatalf("GetFile for nonexistent failed: %v", err) } if got != nil { t.Error("Expected nil for nonexistent file") } // Batch create files := []*File{ {RevisionID: rev.ID, FilePath: "file1.nix", Extension: ".nix", Content: "content1"}, {RevisionID: rev.ID, FilePath: "file2.nix", Extension: ".nix", Content: "content2"}, } if err := store.CreateFilesBatch(ctx, files); err != nil { t.Fatalf("CreateFilesBatch failed: %v", err) } for _, f := range files { if f.ID == 0 { t.Errorf("Expected file %q ID to be set", f.FilePath) } } } func testSchemaVersion(t *testing.T, store Store) { ctx := context.Background() // First initialization if err := store.Initialize(ctx); err != nil { t.Fatalf("First Initialize failed: %v", err) } // Create some data rev := &Revision{GitHash: "version123", ChannelName: "test"} if err := store.CreateRevision(ctx, rev); err != nil { t.Fatalf("CreateRevision failed: %v", err) } // Second initialization should preserve data (same version) if err := store.Initialize(ctx); err != nil { t.Fatalf("Second Initialize failed: %v", err) } got, err := store.GetRevision(ctx, "version123") if err != nil { t.Fatalf("GetRevision after second init failed: %v", err) } if got == nil { t.Error("Data should be preserved after second initialization") } } // TestParentPath tests the ParentPath helper function. func TestParentPath(t *testing.T) { tests := []struct { name string input string expect string }{ {"top level", "services", ""}, {"one level", "services.nginx", "services"}, {"two levels", "services.nginx.enable", "services.nginx"}, {"three levels", "services.nginx.virtualHosts.default", "services.nginx.virtualHosts"}, {"empty", "", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ParentPath(tt.input) if got != tt.expect { t.Errorf("ParentPath(%q) = %q, want %q", tt.input, got, tt.expect) } }) } }