Files
member-console/internal/instance/settings_db_test.go
T
cgalo5758 d1ac278e6b Open every test database through dbtest
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.
2026-10-07 16:22:18 -05:00

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)
}
}