Files
member-console/internal/instance/settings_db_test.go
T

210 lines
6.6 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"
"os"
"testing"
_ "github.com/jackc/pgx/v5/stdlib"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
"git.coopcloud.tech/wiki-cafe/member-console/internal/instance"
"git.coopcloud.tech/wiki-cafe/member-console/internal/migrate"
)
// 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) {
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("TEST_DATABASE_URL not set, skipping integration test")
}
database, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() { database.Close() })
if err := db.RunMigrations(database, migrate.Sources()); err != nil {
t.Fatalf("run migrations: %v", err)
}
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) {
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("TEST_DATABASE_URL not set, skipping integration test")
}
database, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() { database.Close() })
if err := db.RunMigrations(database, migrate.Sources()); err != nil {
t.Fatalf("run migrations: %v", err)
}
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) {
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("TEST_DATABASE_URL not set, skipping integration test")
}
database, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() { database.Close() })
if err := db.RunMigrations(database, migrate.Sources()); err != nil {
t.Fatalf("run migrations: %v", err)
}
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)
}
}