internal/testkit/dbtest opens TEST_DATABASE_URL and E2E_DATABASE_URL, never migrates, and fails with "the test database is behind; run test/reset-test-db.sh" when the core ledger lacks the highest version of db.BaseSources(). The opener helpers except the scratch-database and container openers (26 of the 29) and the inline openers of the database tests call it; helpers that did more than open (providers, fake farms, the FedWiki provider row, cleanup of override rows) keep that setup. The stripe/workflows and workflows/entitlements openers keep their search_path through WithSearchPath. The closed-database seam tests use Closed. TestMigration18SetsExistingFedwikiRulesToForceReduce runs on a scratch database through Scratch and UpTo instead of rolling the shared core ledger back through goose's global state, because no opener migrates the shared database any more. The in-package forms tests of internal/server reached the integrations' forms only because an external test file imported internal/migrate; a test file now links internal/integrations explicitly. TestRegisterProviders_DB is re-pinned from gocognit 37 to 34. New tests: TestLedgerCheckRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase (dbtest), TestOpenRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase (dbtest, through opened, the path Open and OpenE2E share), TestKitPackagesReachOnlyTheirAllowedModulePackages (testkit). The skip messages of the openers are unified to "TEST_DATABASE_URL not set, skipping integration test". 4 breaks: Open skipping its ledger check and the ledger check accepting an empty database fail the new tests; a kit row that no longer allows internal/testkit fails the import guard; leaving the fedwiki rules as they were in migration 00018 fails the migration 18 test on both sides.
167 lines
5.5 KiB
Go
167 lines
5.5 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package instance_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/instance"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
|
|
)
|
|
|
|
// TestStore_DB exercises the instance settings store against a real
|
|
// database: an unset key reads false with no error, SetBool round-trips
|
|
// through GetBool, and a second SetBool overwrites both the value and the
|
|
// recorded actor (the same upsert shape core.integration_config_overrides
|
|
// uses).
|
|
func TestStore_DB(t *testing.T) {
|
|
database := dbtest.Open(t)
|
|
|
|
ctx := context.Background()
|
|
store := instance.NewStore(database)
|
|
t.Cleanup(func() {
|
|
if _, err := database.ExecContext(ctx,
|
|
`DELETE FROM core.instance_settings WHERE key = $1`, string(instance.SetupBannerDismissed)); err != nil {
|
|
t.Errorf("cleanup: %v", err)
|
|
}
|
|
})
|
|
|
|
got, err := store.GetBool(ctx, instance.SetupBannerDismissed)
|
|
if err != nil {
|
|
t.Fatalf("GetBool on unset key: %v", err)
|
|
}
|
|
if got {
|
|
t.Errorf("GetBool on unset key = true, want false")
|
|
}
|
|
|
|
if err := store.SetBool(ctx, instance.SetupBannerDismissed, true, "op@example.com"); err != nil {
|
|
t.Fatalf("SetBool: %v", err)
|
|
}
|
|
got, err = store.GetBool(ctx, instance.SetupBannerDismissed)
|
|
if err != nil {
|
|
t.Fatalf("GetBool after SetBool: %v", err)
|
|
}
|
|
if !got {
|
|
t.Errorf("GetBool after SetBool(true) = false, want true")
|
|
}
|
|
|
|
var updatedBy sql.NullString
|
|
if err := database.QueryRowContext(ctx,
|
|
`SELECT updated_by FROM core.instance_settings WHERE key = $1`, string(instance.SetupBannerDismissed),
|
|
).Scan(&updatedBy); err != nil {
|
|
t.Fatalf("read updated_by: %v", err)
|
|
}
|
|
if !updatedBy.Valid || updatedBy.String != "op@example.com" {
|
|
t.Errorf("updated_by = %+v, want op@example.com", updatedBy)
|
|
}
|
|
|
|
// Overwrite: value flips back to false, actor changes.
|
|
if err := store.SetBool(ctx, instance.SetupBannerDismissed, false, "other@example.com"); err != nil {
|
|
t.Fatalf("SetBool (update path): %v", err)
|
|
}
|
|
got, err = store.GetBool(ctx, instance.SetupBannerDismissed)
|
|
if err != nil {
|
|
t.Fatalf("GetBool after second SetBool: %v", err)
|
|
}
|
|
if got {
|
|
t.Errorf("GetBool after SetBool(false) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestStore_UndeclaredKey covers the registry guard: a key not declared in
|
|
// the package's registry is refused rather than silently read or written,
|
|
// so a typo cannot create an ad hoc setting.
|
|
func TestStore_UndeclaredKey(t *testing.T) {
|
|
database := dbtest.Open(t)
|
|
|
|
store := instance.NewStore(database)
|
|
ctx := context.Background()
|
|
|
|
if _, err := store.GetBool(ctx, instance.Key("not_a_real_setting")); err == nil {
|
|
t.Error("GetBool on an undeclared key returned no error")
|
|
}
|
|
if err := store.SetBool(ctx, instance.Key("not_a_real_setting"), true, ""); err == nil {
|
|
t.Error("SetBool on an undeclared key returned no error")
|
|
}
|
|
}
|
|
|
|
// TestStore_JSON_DB exercises the JSON-object kind against a real
|
|
// database: an unwritten key reads as not-present rather than as an empty
|
|
// object, SetJSON round-trips through GetJSON, and a second SetJSON
|
|
// replaces the whole object and the recorded actor.
|
|
func TestStore_JSON_DB(t *testing.T) {
|
|
database := dbtest.Open(t)
|
|
|
|
ctx := context.Background()
|
|
store := instance.NewStore(database)
|
|
t.Cleanup(func() {
|
|
if _, err := database.ExecContext(ctx,
|
|
`DELETE FROM core.instance_settings WHERE key = $1`, string(instance.StripeEnvironmentCheck)); err != nil {
|
|
t.Errorf("cleanup: %v", err)
|
|
}
|
|
})
|
|
|
|
type record struct {
|
|
KeyFingerprint string `json:"key_fingerprint"`
|
|
Checked int `json:"checked"`
|
|
Stale int `json:"stale"`
|
|
}
|
|
|
|
if _, ok, err := store.GetJSON(ctx, instance.StripeEnvironmentCheck); err != nil {
|
|
t.Fatalf("GetJSON on unset key: %v", err)
|
|
} else if ok {
|
|
t.Error("GetJSON on unset key reported a record")
|
|
}
|
|
|
|
if err := store.SetJSON(ctx, instance.StripeEnvironmentCheck,
|
|
record{KeyFingerprint: "abc123", Checked: 36, Stale: 2}, "op@example.com"); err != nil {
|
|
t.Fatalf("SetJSON: %v", err)
|
|
}
|
|
|
|
raw, ok, err := store.GetJSON(ctx, instance.StripeEnvironmentCheck)
|
|
if err != nil {
|
|
t.Fatalf("GetJSON after SetJSON: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("GetJSON after SetJSON reported no record")
|
|
}
|
|
var got record
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
t.Fatalf("decode stored record: %v", err)
|
|
}
|
|
if got.KeyFingerprint != "abc123" || got.Checked != 36 || got.Stale != 2 {
|
|
t.Errorf("stored record = %+v, want {abc123 36 2}", got)
|
|
}
|
|
|
|
var updatedBy sql.NullString
|
|
if err := database.QueryRowContext(ctx,
|
|
`SELECT updated_by FROM core.instance_settings WHERE key = $1`, string(instance.StripeEnvironmentCheck),
|
|
).Scan(&updatedBy); err != nil {
|
|
t.Fatalf("read updated_by: %v", err)
|
|
}
|
|
if !updatedBy.Valid || updatedBy.String != "op@example.com" {
|
|
t.Errorf("updated_by = %+v, want op@example.com", updatedBy)
|
|
}
|
|
|
|
// Overwrite: the whole object is replaced, not merged.
|
|
if err := store.SetJSON(ctx, instance.StripeEnvironmentCheck,
|
|
record{KeyFingerprint: "def456"}, "other@example.com"); err != nil {
|
|
t.Fatalf("SetJSON (update path): %v", err)
|
|
}
|
|
raw, _, err = store.GetJSON(ctx, instance.StripeEnvironmentCheck)
|
|
if err != nil {
|
|
t.Fatalf("GetJSON after second SetJSON: %v", err)
|
|
}
|
|
got = record{}
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
t.Fatalf("decode replaced record: %v", err)
|
|
}
|
|
if got.KeyFingerprint != "def456" || got.Checked != 0 || got.Stale != 0 {
|
|
t.Errorf("replaced record = %+v, want {def456 0 0}", got)
|
|
}
|
|
}
|