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.
168 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|