diff --git a/internal/overlay/executor_test.go b/internal/overlay/executor_test.go index b0beb771..1b96479c 100644 --- a/internal/overlay/executor_test.go +++ b/internal/overlay/executor_test.go @@ -243,9 +243,41 @@ func TestApplyRejectsExistingOutputDir(t *testing.T) { } } +func TestApplyRejectsSourceFile(t *testing.T) { + sourceDir := filepath.Join(t.TempDir(), "source.txt") + if err := os.WriteFile(sourceDir, []byte("not a dir"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + _, err := Apply(context.Background(), ApplyRequest{ + PackDir: t.TempDir(), + SourceDir: sourceDir, + Overlays: []pack.HostOverlay{{OverlayID: "sample", PatchPath: "overlays/sample.patch"}}, + }) + if err == nil || !strings.Contains(err.Error(), "must be a directory") { + t.Fatalf("Apply() error = %v, want source dir rejection", err) + } +} + func TestCopyFileRejectsMissingSource(t *testing.T) { err := copyFile(filepath.Join(t.TempDir(), "missing.txt"), filepath.Join(t.TempDir(), "target.txt"), 0o644) if err == nil { t.Fatal("copyFile() error = nil, want missing source failure") } } + +func TestCopyFileRejectsTargetParentThatIsAFile(t *testing.T) { + parentFile := filepath.Join(t.TempDir(), "parent-file") + if err := os.WriteFile(parentFile, []byte("block mkdir"), 0o644); err != nil { + t.Fatalf("WriteFile(parentFile) error = %v", err) + } + sourcePath := filepath.Join(t.TempDir(), "source.txt") + if err := os.WriteFile(sourcePath, []byte("hello"), 0o644); err != nil { + t.Fatalf("WriteFile(sourcePath) error = %v", err) + } + + err := copyFile(sourcePath, filepath.Join(parentFile, "nested", "target.txt"), 0o644) + if err == nil { + t.Fatal("copyFile() error = nil, want parent path mkdir failure") + } +} diff --git a/internal/routing/sticky_test.go b/internal/routing/sticky_test.go index 081c2b33..abaefb53 100644 --- a/internal/routing/sticky_test.go +++ b/internal/routing/sticky_test.go @@ -436,6 +436,91 @@ func TestRedisStickyStoreRequiresAddr(t *testing.T) { } } +func TestRedisStickyStoreOpenRejectsAuthError(t *testing.T) { + t.Parallel() + + server := newScriptedRedisServer(t, func(conn net.Conn, reader *bufio.Reader) { + command, err := readRESPArray(reader) + if err != nil { + return + } + if len(command) > 0 && strings.ToUpper(command[0]) == "AUTH" { + _, _ = io.WriteString(conn, "-ERR invalid password\r\n") + } + }) + defer server.Close() + + store := &RedisStickyStore{cfg: RedisConfig{Addr: server.Addr(), Password: "secret"}} + _, _, err := store.open(context.Background()) + if err == nil || !strings.Contains(err.Error(), "redis AUTH read") { + t.Fatalf("open() error = %v, want auth read failure", err) + } +} + +func TestRedisStickyStoreOpenRejectsSelectUnexpectedResponse(t *testing.T) { + t.Parallel() + + server := newScriptedRedisServer(t, func(conn net.Conn, reader *bufio.Reader) { + command, err := readRESPArray(reader) + if err != nil { + return + } + if len(command) > 0 && strings.ToUpper(command[0]) == "AUTH" { + _, _ = io.WriteString(conn, "+OK\r\n") + } + command, err = readRESPArray(reader) + if err != nil { + return + } + if len(command) > 0 && strings.ToUpper(command[0]) == "SELECT" { + _, _ = io.WriteString(conn, ":1\r\n") + } + }) + defer server.Close() + + store := &RedisStickyStore{cfg: RedisConfig{Addr: server.Addr(), Password: "secret", DB: 2}} + _, _, err := store.open(context.Background()) + if err == nil || !strings.Contains(err.Error(), "redis SELECT unexpected response") { + t.Fatalf("open() error = %v, want select unexpected response", err) + } +} + +func TestRedisStickyStoreGetJSONRejectsUnexpectedResponse(t *testing.T) { + t.Parallel() + + server := newScriptedRedisServer(t, func(conn net.Conn, reader *bufio.Reader) { + if _, err := readRESPArray(reader); err != nil { + return + } + _, _ = io.WriteString(conn, "+OK\r\n") + }) + defer server.Close() + + store := &RedisStickyStore{cfg: RedisConfig{Addr: server.Addr()}} + _, _, err := store.getJSON(context.Background(), "sticky-key") + if err == nil || !strings.Contains(err.Error(), "unexpected response") { + t.Fatalf("getJSON() error = %v, want unexpected response", err) + } +} + +func TestRedisStickyStoreSetJSONRejectsUnexpectedResponse(t *testing.T) { + t.Parallel() + + server := newScriptedRedisServer(t, func(conn net.Conn, reader *bufio.Reader) { + if _, err := readRESPArray(reader); err != nil { + return + } + _, _ = io.WriteString(conn, ":1\r\n") + }) + defer server.Close() + + store := &RedisStickyStore{cfg: RedisConfig{Addr: server.Addr()}} + err := store.setJSON(context.Background(), "sticky-key", map[string]string{"route_id": "asxs"}, time.Second) + if err == nil || !strings.Contains(err.Error(), "unexpected response") { + t.Fatalf("setJSON() error = %v, want unexpected response", err) + } +} + func TestNormalizeRuntimeBackend(t *testing.T) { t.Parallel() @@ -470,3 +555,35 @@ func TestRedisStickyStoreFixturePathExists(t *testing.T) { t.Fatal("temp dir base should not be empty") } } + +type scriptedRedisServer struct { + listener net.Listener + handler func(net.Conn, *bufio.Reader) +} + +func newScriptedRedisServer(t *testing.T, handler func(net.Conn, *bufio.Reader)) *scriptedRedisServer { + t.Helper() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("net.Listen() error = %v", err) + } + server := &scriptedRedisServer{listener: ln, handler: handler} + go func() { + conn, err := ln.Accept() + if err != nil { + return + } + defer conn.Close() + handler(conn, bufio.NewReader(conn)) + }() + return server +} + +func (s *scriptedRedisServer) Addr() string { + return s.listener.Addr().String() +} + +func (s *scriptedRedisServer) Close() { + _ = s.listener.Close() +} diff --git a/internal/store/sqlite/import_runs_repo_test.go b/internal/store/sqlite/import_runs_repo_test.go index dea737e3..43125282 100644 --- a/internal/store/sqlite/import_runs_repo_test.go +++ b/internal/store/sqlite/import_runs_repo_test.go @@ -281,6 +281,88 @@ func TestImportRunItemsRepoCreateUpdateAndLease(t *testing.T) { } } +func TestImportRunItemsRepoUpsertDefaultsOptionalJSONAndNullableFields(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store := openTestDB(t) + + run := ImportRun{RunID: "run-upsert-defaults", HostID: "host-upsert", Mode: "strict", AccessMode: "subscription", State: "running"} + if err := store.ImportRuns().Create(ctx, run); err != nil { + t.Fatalf("ImportRuns().Create() error = %v", err) + } + + if err := store.ImportRunItems().Upsert(ctx, ImportRunItem{ + ItemID: "item-upsert-defaults", + RunID: "run-upsert-defaults", + BaseURL: "https://api.example.com/v1", + ProviderID: "provider-upsert", + APIKeyFingerprint: "fp-upsert", + CurrentStage: "confirm", + ConfirmationStatus: "pending", + AccessStatus: "unknown", + MatchedAccountState: "active", + AccountResolution: "created", + ResolvedSmokeModel: " gpt-5.4 ", + LastRetryAt: " ", + NextRetryAt: " ", + LeaseOwner: " worker-upsert ", + LeaseUntil: " 2026-05-23T12:00:00Z ", + LastErrorStage: " confirm ", + LastError: " timeout ", + LegacyProviderID: " legacy-provider ", + }); err != nil { + t.Fatalf("ImportRunItems().Upsert() error = %v", err) + } + + got, err := store.ImportRunItems().GetByItemID(ctx, "item-upsert-defaults") + if err != nil { + t.Fatalf("ImportRunItems().GetByItemID() error = %v", err) + } + if got.RequestedModelsJSON != "[]" { + t.Fatalf("RequestedModelsJSON = %q, want []", got.RequestedModelsJSON) + } + if got.RawModelsJSON != "[]" { + t.Fatalf("RawModelsJSON = %q, want []", got.RawModelsJSON) + } + if got.NormalizedModelsJSON != "[]" { + t.Fatalf("NormalizedModelsJSON = %q, want []", got.NormalizedModelsJSON) + } + if got.CanonicalFamiliesJSON != "[]" { + t.Fatalf("CanonicalFamiliesJSON = %q, want []", got.CanonicalFamiliesJSON) + } + if got.RecommendedModelsJSON != "[]" { + t.Fatalf("RecommendedModelsJSON = %q, want []", got.RecommendedModelsJSON) + } + if got.CapabilityProfileJSON != "{}" { + t.Fatalf("CapabilityProfileJSON = %q, want {}", got.CapabilityProfileJSON) + } + if got.AdvisoryMessagesJSON != "[]" { + t.Fatalf("AdvisoryMessagesJSON = %q, want []", got.AdvisoryMessagesJSON) + } + if got.ResolvedSmokeModel != "gpt-5.4" { + t.Fatalf("ResolvedSmokeModel = %q, want gpt-5.4", got.ResolvedSmokeModel) + } + if got.LeaseOwner != "worker-upsert" { + t.Fatalf("LeaseOwner = %q, want worker-upsert", got.LeaseOwner) + } + if got.LeaseUntil != "2026-05-23T12:00:00Z" { + t.Fatalf("LeaseUntil = %q, want trimmed lease_until", got.LeaseUntil) + } + if got.LastErrorStage != "confirm" { + t.Fatalf("LastErrorStage = %q, want confirm", got.LastErrorStage) + } + if got.LastError != "timeout" { + t.Fatalf("LastError = %q, want timeout", got.LastError) + } + if got.LegacyProviderID != "legacy-provider" { + t.Fatalf("LegacyProviderID = %q, want legacy-provider", got.LegacyProviderID) + } + if got.LastRetryAt != "" || got.NextRetryAt != "" { + t.Fatalf("retry timestamps = (%q, %q), want empty strings", got.LastRetryAt, got.NextRetryAt) + } +} + func TestImportRunEventsRepoCreateAndHelpers(t *testing.T) { t.Parallel() diff --git a/internal/store/sqlite/packs_repo_test.go b/internal/store/sqlite/packs_repo_test.go index 8277af38..386d79e2 100644 --- a/internal/store/sqlite/packs_repo_test.go +++ b/internal/store/sqlite/packs_repo_test.go @@ -113,6 +113,52 @@ func TestPacksRepoUpsertUpdatesExisting(t *testing.T) { } } +func TestPacksRepoUpsertTrimsAndDefaultsManifest(t *testing.T) { + store := openTestDB(t) + + id, err := store.Packs().Upsert(context.Background(), Pack{ + PackID: " upsert-pack-json ", + Version: " 1.2.3 ", + Checksum: " chk-json ", + Vendor: " vendor-a ", + TargetHost: " sub2api ", + MinHostVersion: " 0.1.0 ", + MaxHostVersion: " 0.2.x ", + }) + if err != nil { + t.Fatalf("Upsert() error = %v", err) + } + + got, err := store.Packs().GetByID(context.Background(), id) + if err != nil { + t.Fatalf("GetByID() error = %v", err) + } + if got.PackID != "upsert-pack-json" { + t.Fatalf("PackID = %q, want upsert-pack-json", got.PackID) + } + if got.Version != "1.2.3" { + t.Fatalf("Version = %q, want 1.2.3", got.Version) + } + if got.Checksum != "chk-json" { + t.Fatalf("Checksum = %q, want chk-json", got.Checksum) + } + if got.Vendor != "vendor-a" { + t.Fatalf("Vendor = %q, want vendor-a", got.Vendor) + } + if got.TargetHost != "sub2api" { + t.Fatalf("TargetHost = %q, want sub2api", got.TargetHost) + } + if got.MinHostVersion != "0.1.0" { + t.Fatalf("MinHostVersion = %q, want 0.1.0", got.MinHostVersion) + } + if got.MaxHostVersion != "0.2.x" { + t.Fatalf("MaxHostVersion = %q, want 0.2.x", got.MaxHostVersion) + } + if got.ManifestJSON != "{}" { + t.Fatalf("ManifestJSON = %q, want {}", got.ManifestJSON) + } +} + func TestPacksRepoValidationErrors(t *testing.T) { store := openTestDB(t) diff --git a/internal/store/sqlite/provider_accounts_repo_test.go b/internal/store/sqlite/provider_accounts_repo_test.go index 0a0b481c..731dc24c 100644 --- a/internal/store/sqlite/provider_accounts_repo_test.go +++ b/internal/store/sqlite/provider_accounts_repo_test.go @@ -325,6 +325,27 @@ func TestSyncProviderAccountsFromImportBatchPreservesManualDisabledStatus(t *tes } } +func TestNormalizeProviderAccountBindingState(t *testing.T) { + t.Parallel() + + tests := []struct { + input string + want string + }{ + {input: ProviderAccountBindingStateAssigned, want: ProviderAccountBindingStateAssigned}, + {input: " " + ProviderAccountBindingStateUnassigned + " ", want: ProviderAccountBindingStateUnassigned}, + {input: ProviderAccountBindingStateConflict, want: ProviderAccountBindingStateConflict}, + {input: "invalid", want: ""}, + {input: " ", want: ""}, + } + + for _, tt := range tests { + if got := normalizeProviderAccountBindingState(tt.input); got != tt.want { + t.Fatalf("normalizeProviderAccountBindingState(%q) = %q, want %q", tt.input, got, tt.want) + } + } +} + func TestSyncProviderAccountsFromImportBatchInfersRouteFromShadowBinding(t *testing.T) { t.Parallel() diff --git a/internal/store/sqlite/providers_repo_test.go b/internal/store/sqlite/providers_repo_test.go index 089a2390..2c5223d3 100644 --- a/internal/store/sqlite/providers_repo_test.go +++ b/internal/store/sqlite/providers_repo_test.go @@ -170,6 +170,65 @@ func TestProvidersRepoUpsertUpdatesExisting(t *testing.T) { } } +func TestProvidersRepoUpsertTrimsAndDefaultsOptionalJSON(t *testing.T) { + store := openTestDB(t) + packID := createTestPack(t, store) + + id, err := store.Providers().Upsert(context.Background(), Provider{ + PackID: packID, + ProviderID: " upsert-json ", + DisplayName: " Upsert JSON ", + BaseURL: " https://json.example.com/v1 ", + Platform: " openai ", + AccountType: " apikey ", + SmokeTestModel: " gpt-5.4 ", + }) + if err != nil { + t.Fatalf("Upsert() error = %v", err) + } + + got, err := store.Providers().GetByID(context.Background(), id) + if err != nil { + t.Fatalf("GetByID() error = %v", err) + } + if got.ProviderID != "upsert-json" { + t.Fatalf("ProviderID = %q, want upsert-json", got.ProviderID) + } + if got.DisplayName != "Upsert JSON" { + t.Fatalf("DisplayName = %q, want Upsert JSON", got.DisplayName) + } + if got.BaseURL != "https://json.example.com/v1" { + t.Fatalf("BaseURL = %q, want trimmed base url", got.BaseURL) + } + if got.Platform != "openai" { + t.Fatalf("Platform = %q, want openai", got.Platform) + } + if got.AccountType != "apikey" { + t.Fatalf("AccountType = %q, want apikey", got.AccountType) + } + if got.SmokeTestModel != "gpt-5.4" { + t.Fatalf("SmokeTestModel = %q, want gpt-5.4", got.SmokeTestModel) + } + if got.DefaultModelsJSON != "[]" { + t.Fatalf("DefaultModelsJSON = %q, want []", got.DefaultModelsJSON) + } + if got.GroupTemplateJSON != "{}" { + t.Fatalf("GroupTemplateJSON = %q, want {}", got.GroupTemplateJSON) + } + if got.ChannelTemplateJSON != "{}" { + t.Fatalf("ChannelTemplateJSON = %q, want {}", got.ChannelTemplateJSON) + } + if got.PlanTemplateJSON != "{}" { + t.Fatalf("PlanTemplateJSON = %q, want {}", got.PlanTemplateJSON) + } + if got.ImportOptionsJSON != "{}" { + t.Fatalf("ImportOptionsJSON = %q, want {}", got.ImportOptionsJSON) + } + if got.ManifestJSON != "{}" { + t.Fatalf("ManifestJSON = %q, want {}", got.ManifestJSON) + } +} + func TestProvidersRepoValidationErrors(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store)