Files
member-console/internal/integration/registration_db_test.go
T
cgalo5758 259c935981 Unify operator integration management
List every provider kind with direct settings and admin links, move
FedWiki
under the integrations route, and add in-shell operator 404s.

Report sync health from Temporal schedule executions and clear one-shot
settings feedback parameters after display.
2026-07-23 00:14:21 -05:00

168 lines
6.8 KiB
Go

package integration_test
import (
"context"
"database/sql"
"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/integration"
dcmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/discourse/store"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
stripemod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/stripe/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/migrate"
)
// TestRegisterProviders_DB exercises the full boot registration path against a
// real database: the same migration assembly as cmd/start (so version numbering
// is self-consistent), then RegisterProviders with the real FedWiki + Stripe
// sources. It asserts the registry rows, the operation set, and the cross-schema
// stamp of core.resource_keys.provider — the path that would os.Exit(1)
// at boot if broken.
//
// Uses the full module assembly; run it against an isolated/fresh database, not
// alongside the partial-assembly entitlements/provisioning suites on a shared DB
// (see the test-isolation issue in status/issues.md).
func TestRegisterProviders_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() })
sources := migrate.Sources()
if err := db.RunMigrations(database, sources); err != nil {
t.Fatalf("run migrations: %v", err)
}
ctx := context.Background()
srcs := []integration.ProviderSource{fwmod.ProviderSource(), stripemod.ProviderSource(), dcmod.ProviderSource()}
if err := integration.RegisterProviders(ctx, database, srcs); err != nil {
t.Fatalf("RegisterProviders: %v", err)
}
// Provider rows + kinds.
for slug, wantKind := range map[string]string{"fedwiki": "provisioning", "stripe": "payment", "discourse": "provisioning"} {
var kind string
if err := database.QueryRowContext(ctx,
"SELECT provider_kind FROM core.providers WHERE slug=$1", slug).Scan(&kind); err != nil {
t.Fatalf("query provider %q: %v", slug, err)
}
if kind != wantKind {
t.Errorf("provider %q kind = %q, want %q", slug, kind, wantKind)
}
}
// FedWiki declares exactly create/set_status/delete/list/describe.
var opCount int
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.provider_operations WHERE provider='fedwiki'").Scan(&opCount); err != nil {
t.Fatalf("count fedwiki operations: %v", err)
}
if opCount != 5 {
t.Errorf("fedwiki operation count = %d, want 5 (create/set_status/delete/list/describe)", opCount)
}
// FedWiki declares lifecycle states active/readonly/archived; Stripe (no
// set_status) declares none.
var fwStates, stripeStates int
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.provider_states WHERE provider='fedwiki'").Scan(&fwStates); err != nil {
t.Fatalf("count fedwiki states: %v", err)
}
if fwStates != 3 {
t.Errorf("fedwiki state count = %d, want 3 (active/readonly/archived)", fwStates)
}
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.provider_states WHERE provider='stripe'").Scan(&stripeStates); err != nil {
t.Fatalf("count stripe states: %v", err)
}
if stripeStates != 0 {
t.Errorf("stripe state count = %d, want 0 (no set_status)", stripeStates)
}
// FedWiki's operator surface path is registered (drives the registry-driven nav).
var surface sql.NullString
if err := database.QueryRowContext(ctx,
"SELECT operator_surface_path FROM core.providers WHERE slug='fedwiki'").Scan(&surface); err != nil {
t.Fatalf("query fedwiki surface path: %v", err)
}
if !surface.Valid || surface.String != "/operator/integrations/fedwiki" {
t.Errorf("fedwiki operator_surface_path = %v, want '/operator/integrations/fedwiki'", surface)
}
// The cross-schema stamp landed on the renamed key.
var provider sql.NullString
if err := database.QueryRowContext(ctx,
"SELECT provider FROM core.resource_keys WHERE resource_key='fedwiki_sites'").Scan(&provider); err != nil {
t.Fatalf("query fedwiki_sites provider: %v", err)
}
if !provider.Valid || provider.String != "fedwiki" {
t.Errorf("fedwiki_sites.provider = %v, want 'fedwiki'", provider)
}
// The bare 'sites' key is gone (renamed by the fwmod migration). Use a
// query parameter, not a literal, so this absence check doesn't trip the
// retired-resource-key lint.
var staleCount int
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.resource_keys WHERE resource_key = $1", "sites").Scan(&staleCount); err != nil {
t.Fatalf("count stale resource key: %v", err)
}
if staleCount != 0 {
t.Errorf("bare 'sites' resource key still present (count=%d)", staleCount)
}
// Discourse declares only the read class (list/describe) and no states:
// group membership converges via reconciliation, not operator-invoked
// lifecycle verbs (see the discourse-integration change's design D6).
var dcOps, dcStates int
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.provider_operations WHERE provider='discourse'").Scan(&dcOps); err != nil {
t.Fatalf("count discourse operations: %v", err)
}
if dcOps != 2 {
t.Errorf("discourse operation count = %d, want 2 (list/describe)", dcOps)
}
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.provider_states WHERE provider='discourse'").Scan(&dcStates); err != nil {
t.Fatalf("count discourse states: %v", err)
}
if dcStates != 0 {
t.Errorf("discourse state count = %d, want 0 (no set_status)", dcStates)
}
// Discourse's operator surface path drives the registry-driven nav.
var dcSurface sql.NullString
if err := database.QueryRowContext(ctx,
"SELECT operator_surface_path FROM core.providers WHERE slug='discourse'").Scan(&dcSurface); err != nil {
t.Fatalf("query discourse surface path: %v", err)
}
if !dcSurface.Valid || dcSurface.String != "/operator/integrations/discourse" {
t.Errorf("discourse operator_surface_path = %v, want '/operator/integrations/discourse'", dcSurface)
}
// The stamp landed on the key seeded by the discourse stream's own 00001.
var dcProvider sql.NullString
if err := database.QueryRowContext(ctx,
"SELECT provider FROM core.resource_keys WHERE resource_key='discourse_posting'").Scan(&dcProvider); err != nil {
t.Fatalf("query discourse_posting provider: %v", err)
}
if !dcProvider.Valid || dcProvider.String != "discourse" {
t.Errorf("discourse_posting.provider = %v, want 'discourse'", dcProvider)
}
// Idempotent: a second registration is a clean no-op.
if err := integration.RegisterProviders(ctx, database, srcs); err != nil {
t.Fatalf("second RegisterProviders (idempotency): %v", err)
}
}