Build the FedWiki tests' fixtures through the kit

Adds internal/testkit/world (New, Providers, Person, Org, Workspace,
Product, Rule, Confer, Person.SignIn), internal/testkit/authtest
(NewSessionManager, SignIn) and internal/integrations/fedwiki/fedwikitest
(the environment, tenant, limit and usage, operator root, site rows), and
moves the FedWiki fixtures onto them: newReqEnv, setupQuotaFixture,
newReconcileTestEnv, siblingWorkspace, governFedwikiLimit (Product, Rule,
Confer, no materialization), newSiteEnv, newClaimTestDB (its raw provider
insert goes), seedOperatorSites, newMemberWorkspace, seedUnderstatedQuota,
newTestWorkspace, seedCountedWorkspace and addSiblingWorkspace.
newReconcileDomainsDB and setupReconcileRoot had nothing left to move. The
six sessions the FedWiki web tests build go through authtest. The import
guard covers the three packages, and scripts/deadcode-testonly.txt lists
them.

The six hand-written lock-wait loops become dbtest.AwaitLockWait
(lifecycle_db_test.go, registration_db_test.go, invoice_reconcile_test.go,
stamp_db_test.go, and the two record-swap tests in requests_db_test.go and
swap_record_db_test.go). Backend gains BlockedBy, Query and Abort. The
registration loop is not a verbatim move: its select matches by query text
within the current database and its done check becomes the Abort probe, as
do the two record-swap loops, each keeping its message ("registration
finished (%v) while the first key was held", "the record finished without
writing the activated site's status: %v", "the record finished without
waiting on the parked request: %v"). Kept weaker on purpose: the
never-waited messages are now the helper's, which names the selection
("backend N never waited on a lock" for the pid loops, "no backend waited on
a lock held by backend N" for the record swaps, "no backend running a
statement containing %q waited on a lock" for the registration and invoice
loops), the record-swap and stamp bounds are the helper's ten seconds (were
the tests' thirty), the stamp loop accepts any waiting count (was exactly
one for its pid), and the invoice loop matches the statement text anywhere
in the query (was its prefix). TestDispatchStampsLockRecordsInOneOrder drops
from cognitive complexity 39 to 31; its pin is re-pinned to 31.

Names and results of the three FedWiki packages, internal/integration,
internal/workflows/desiredstate and internal/integrations/stripe/workflows
equal the parent's apart from the new tests (the import guard's forbidden
list with its three subtests, and the lock helper's selection test with its
three subtests and its Abort test) and one change: the three
TestReconcileDomains* tests in internal/integrations/fedwiki now pass on a
freshly reset database. They failed there at the parent, alone and in the
package run, because nothing had registered FedWiki; the fixture now does
(#33).

Fixture equivalence (each fixture run alone on a reset database, every table
of core, fedwiki and domains dumped before and after). Differences, none
read by an assertion: names and addresses carry the builder's shape
(organization "<name> <suffix>", email "<name>-<suffix>@example.test",
subject "sub-<name>-<suffix>"; the display name is the label for tenant
fixtures); the pool is the organization's default pool (name "Default", key
default, auto-managed) where the fixtures wrote a bare one; a workspace now
sits in the organization's default pool through a pool assignment where
newMemberWorkspace, seedOperatorSites and the claim fixtures wrote neither
(seedOperatorSites gains two pools and two assignments); every FedWiki
fixture registers FedWiki and the System tenant, which adds the provider row
(or fills operator_surface_path on the row a claim fixture inserted), the
operations, connection, state and outbox rows, the System tenant's user,
person, 'system' organization type, organization and workspace, and
refreshes the fedwiki_sites resource key; archived sites carry archived_at,
as the status query sets it; sites are minted under a database-minted id;
governing a limit goes through the rule-change function, which adds a
set-change row. Claim tokens are random in both.

25 breaks: 19 failed the old test at the parent and the new test here (one
code break per FedWiki test file in the function it pins, and one per
converted lock wait with the lock removed; the three loops with an Abort
probe fail through it with their own message), and six on the kit alone
failed the new test. No assertion in a test body changed.
This commit is contained in:
2026-10-08 02:43:55 -05:00
parent addd318be3
commit f8f9551ddd
25 changed files with 803 additions and 501 deletions
+1 -1
View File
@@ -330,7 +330,7 @@ linters:
text: 'cognitive complexity 37 of func `processGroup`' # #166 step 6
- path: '^internal/workflows/desiredstate/stamp_db_test\.go$'
linters: [gocognit]
text: 'cognitive complexity 39 of func `TestDispatchStampsLockRecordsInOneOrder`' # #166 step 6
text: 'cognitive complexity 31 of func `TestDispatchStampsLockRecordsInOneOrder`' # #166 step 6
- path: '^internal/workflows/domains/verify\.go$'
linters: [gocognit]
text: 'cognitive complexity 36 of func `VerifyClaimWorkflow`' # #166 step 6
+1 -12
View File
@@ -408,18 +408,7 @@ func TestRecordLifecycleRequestConcurrentOlderTab_DB(t *testing.T) {
}()
// Wait until the older recording waits on the instance's lock.
deadline := time.Now().Add(10 * time.Second)
for {
var waiting string
_ = database.QueryRowContext(ctx, `SELECT COALESCE(wait_event_type, '') FROM pg_stat_activity WHERE pid = $1`, olderPID).Scan(&waiting)
if waiting == "Lock" {
break
}
if time.Now().After(deadline) {
t.Fatal("the older recording never waited on the instance lock")
}
time.Sleep(20 * time.Millisecond)
}
dbtest.AwaitLockWait(t, database, dbtest.Backend{PID: olderPID})
if err := integration.FinishLifecycleRequest(ctx, newer, integration.LifecycleOutcome{
InstanceID: instance, Generation: g2, Status: integration.LifecycleSucceeded, FinishedAt: time.Now()}); err != nil {
+4 -18
View File
@@ -563,28 +563,14 @@ func TestRegisterProvidersStampsKeysInByteOrder_DB(t *testing.T) {
}()
// Wait until registration waits on the first key.
deadline := time.Now().Add(10 * time.Second)
for {
var waiting int
if err := database.QueryRowContext(ctx,
`SELECT COUNT(*) FROM pg_stat_activity
WHERE datname = current_database() AND wait_event_type = 'Lock'
AND query LIKE '%UPDATE core.resource_keys%'`).Scan(&waiting); err != nil {
t.Fatal(err)
}
if waiting > 0 {
break
}
dbtest.AwaitLockWait(t, database, dbtest.Backend{Query: "UPDATE core.resource_keys", Abort: func() error {
select {
case err := <-done:
t.Fatalf("registration finished (%v) while the first key was held", err)
return fmt.Errorf("registration finished (%v) while the first key was held", err)
default:
return nil
}
if time.Now().After(deadline) {
t.Fatal("registration never waited on the held key")
}
time.Sleep(20 * time.Millisecond)
}
}})
// Registration has not stamped the second key yet: nothing holds it.
_, lockErr := database.ExecContext(ctx,
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Package fedwikitest holds the fixtures the FedWiki packages' database tests
// share: FedWiki registered as boot registers it, a workspace with its pool, a
// sites limit and a usage figure, the operator root hosted names are carved
// under, and site rows. It imports the FedWiki store and never the web,
// workflows or module packages, so all three packages' tests can import it.
//
// The limit and the usage are written through the entitlements queries, not
// materialized from a rule: the counter tests pin states only a direct write
// reaches, such as usage above the limit or a limit no rule governs.
package fedwikitest
import (
"context"
"database/sql"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/world"
)
// sitesKey is the resource key FedWiki counts its sites under.
const sitesKey = "fedwiki_sites"
// Env is a World with FedWiki registered.
type Env struct {
*world.World
Conn string // id of FedWiki's connection
}
// New registers FedWiki on d, through the World, and reads its connection.
func New(tb testing.TB, d *sql.DB) *Env {
tb.Helper()
w := world.New(tb, d)
w.Providers(fwmod.ProviderSource())
conn, err := integration.New(d).GetConnectionByProvider(w.Ctx, fwmod.ModuleName)
if err != nil {
tb.Fatalf("FedWiki connection: %v", err)
}
return &Env{World: w, Conn: conn.ConnectionID}
}
// Tenant is one member's person, organization and workspace.
type Tenant struct {
Person world.Person
Org world.Org
Workspace world.Workspace
}
// Tenant creates a person, a personal organization and a workspace in it.
func (e *Env) Tenant(name string) Tenant {
e.TB.Helper()
person := e.Person(name)
org := e.Org(name, "personal", person)
return Tenant{Person: person, Org: org, Workspace: e.Workspace(org, "WS")}
}
// Limit gives the tenant's pool a fedwiki_sites limit and returns the
// entitlement's id.
func (e *Env) Limit(t Tenant, limit int64) string {
e.TB.Helper()
ent, err := entitlements.New(e.DB).CreateNumericEntitlement(e.Ctx, entitlements.CreateNumericEntitlementParams{
PoolID: t.Workspace.PoolID, ResourceKey: sitesKey, EntitlementType: "limit", ResourceLimit: limit,
})
if err != nil {
e.TB.Fatalf("fedwiki_sites limit: %v", err)
}
return ent.EntitlementID
}
// Usage gives the entitlement a usage row reading used.
func (e *Env) Usage(t Tenant, entitlementID string, used int64) {
e.TB.Helper()
q := entitlements.New(e.DB)
if _, err := q.CreateNumericEntitlementUsage(e.Ctx, entitlements.CreateNumericEntitlementUsageParams{
EntitlementID: entitlementID, PoolID: t.Workspace.PoolID, ResourceKey: sitesKey,
}); err != nil {
e.TB.Fatalf("fedwiki_sites usage: %v", err)
}
if used == 0 {
return
}
if _, err := q.RaiseNumericUsageTo(e.Ctx, entitlements.RaiseNumericUsageToParams{
Observed: used, PoolID: t.Workspace.PoolID, ResourceKey: sitesKey,
}); err != nil {
e.TB.Fatalf("fedwiki_sites usage %d: %v", used, err)
}
}
// Counted creates a Tenant whose pool has a fedwiki_sites limit and a usage
// row reading used.
func (e *Env) Counted(name string, limit, used int64) Tenant {
e.TB.Helper()
t := e.Tenant(name)
e.Usage(t, e.Limit(t, limit), used)
return t
}
// Sibling adds a workspace to the organization of ws; it draws on the same
// default pool (resource-pools invariant 7: an organization's workspaces draw
// down one pool).
func (e *Env) Sibling(ws, name string) world.Workspace {
e.TB.Helper()
var orgID string
if err := e.DB.QueryRowContext(e.Ctx, `SELECT org_id FROM core.workspaces WHERE workspace_id = $1`, ws).Scan(&orgID); err != nil {
e.TB.Fatalf("organization of workspace %s: %v", ws, err)
}
return e.Workspace(world.Org{ID: orgID}, name)
}
// OperatorRoot ensures an operator root under the System tenant, named by
// label and the World's suffix, and returns its domain.
func (e *Env) OperatorRoot(label string) string {
e.TB.Helper()
systemWS, err := systemtenant.Ensure(e.Ctx, e.DB)
if err != nil {
e.TB.Fatalf("ensure the System tenant: %v", err)
}
root := label + e.Sfx + ".test"
if _, err := domains.NewRegistry(e.DB).EnsureOperatorRoot(e.Ctx, systemWS, root); err != nil {
e.TB.Fatalf("ensure operator root %s: %v", root, err)
}
return root
}
// Govern gives the pool of the organization holding ws an active provision of
// a set whose fedwiki_sites rule carries policy, so a sweep has a governing
// rule to read. The grant is conferred without materializing the pool, which
// keeps the limit the test wrote.
func (e *Env) Govern(ws, policy string) {
e.TB.Helper()
var org world.Org
if err := e.DB.QueryRowContext(e.Ctx,
`SELECT pa.pool_id, rp.org_id, o.owner_person_id FROM core.pool_assignments pa
JOIN core.resource_pools rp ON rp.pool_id = pa.pool_id
JOIN core.organizations o ON o.org_id = rp.org_id
WHERE pa.workspace_id = $1 AND pa.is_primary`, ws).Scan(&org.PoolID, &org.ID, &org.Owner.ID); err != nil {
e.TB.Fatalf("pool of workspace %s: %v", ws, err)
}
p := e.Product("Govern")
e.Rule(p, entitlements.RuleFields{
RuleType: "limit",
ResourceKey: sql.NullString{String: sitesKey, Valid: true},
ResourceValue: sql.NullInt64{Int64: 1, Valid: true},
ResourcePerUnit: sql.NullBool{Bool: false, Valid: true},
StackingPolicy: sql.NullString{String: "additive", Valid: true},
TierReductionPolicy: policy,
})
e.Confer(org, p)
}
// Site creates a site row in ws, under an id the database mints as it does
// for an instance, with the status and returns that id. A status
// other than active is set the way a lifecycle change sets it.
func Site(tb testing.TB, d *sql.DB, ws, domain, status string) string {
tb.Helper()
ctx := context.Background()
id, err := integration.NewInstanceID(ctx, d)
if err != nil {
tb.Fatalf("mint an id for site %s: %v", domain, err)
}
q := fwmod.New(d)
site, err := q.CreateSite(ctx, fwmod.CreateSiteParams{SiteID: id, WorkspaceID: ws, Domain: domain})
if err != nil {
tb.Fatalf("create site %s: %v", domain, err)
}
if status != "active" {
if _, err := q.SetSiteStatusByDomain(ctx, fwmod.SetSiteStatusByDomainParams{Domain: domain, Status: status}); err != nil {
tb.Fatalf("set site %s %s: %v", domain, status, err)
}
}
return site.SiteID
}
// SiteModifiedAt creates an active site row in ws that the farm last modified
// at modified.
func SiteModifiedAt(tb testing.TB, d *sql.DB, ws, domain string, modified time.Time) {
tb.Helper()
if _, err := fwmod.New(d).UpsertSiteFromFarm(context.Background(), fwmod.UpsertSiteFromFarmParams{
WorkspaceID: ws, Domain: domain, Status: "active", LastModifiedAt: sql.NullTime{Time: modified, Valid: true},
}); err != nil {
tb.Fatalf("create site %s: %v", domain, err)
}
}
// ForceReduce marks the site as parked by a downgrade.
func ForceReduce(tb testing.TB, d *sql.DB, domain string) {
tb.Helper()
if err := fwmod.New(d).SetForceReducedByDomain(context.Background(), domain); err != nil {
tb.Fatalf("force-reduce %s: %v", domain, err)
}
}
@@ -17,6 +17,7 @@ import (
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/workflows"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
@@ -33,36 +34,17 @@ var _ interface {
ReconcileDomains(ctx context.Context, database *sql.DB, logger *slog.Logger) error
} = Adapter{}
// newMemberWorkspace builds the org → workspace chain a member site needs.
// newMemberWorkspace builds the org → workspace chain a member site needs,
// and registers FedWiki: a placement references its provider row.
// Brand-neutral throughout: nothing here names a deployment or a product.
func newMemberWorkspace(t *testing.T, database *sql.DB, label string) string {
t.Helper()
ctx := context.Background()
uniq := fmt.Sprintf("%s-%d", label, time.Now().UnixNano())
scan := func(q string, args ...any) string {
var id string
if err := database.QueryRowContext(ctx, q, args...).Scan(&id); err != nil {
t.Fatalf("fixture %q: %v", q, err)
}
return id
}
userID := scan(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+uniq)
personID := scan(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1,$2,$3) RETURNING person_id`,
userID, "Tester", uniq+"@example.com")
orgID := scan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`,
"Org "+uniq, personID)
return scan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,$2) RETURNING workspace_id`,
orgID, "WS")
return fedwikitest.New(t, database).Tenant(label).Workspace.ID
}
func insertSite(t *testing.T, database *sql.DB, workspaceID, domain, status string) {
t.Helper()
if _, err := database.ExecContext(context.Background(),
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1,$2,$3)`,
workspaceID, domain, status); err != nil {
t.Fatalf("insert site %s: %v", domain, err)
}
fedwikitest.Site(t, database, workspaceID, domain, status)
}
// claimAt returns the live claim at root, or nil when none exists.
@@ -13,6 +13,7 @@ import (
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
@@ -23,49 +24,19 @@ import (
// under no materialized numeric entitlement has.
func seedCountedWorkspace(t *testing.T, database *sql.DB, label string, active, archived int, counter int64, withCounter bool) string {
t.Helper()
ctx := context.Background()
uniq := fmt.Sprintf("%s-%d", label, time.Now().UnixNano())
scan := func(q string, args ...any) string {
var id string
if err := database.QueryRowContext(ctx, q, args...).Scan(&id); err != nil {
t.Fatalf("fixture %q: %v", q, err)
}
return id
}
userID := scan(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+uniq)
personID := scan(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1,$2,$3) RETURNING person_id`,
userID, "Tester", uniq+"@example.com")
orgID := scan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`,
"Org "+uniq, personID)
workspaceID := scan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,'WS') RETURNING workspace_id`, orgID)
poolID := scan(`INSERT INTO core.resource_pools (org_id, name, pool_type) VALUES ($1,'Pool','default') RETURNING pool_id`, orgID)
if _, err := database.ExecContext(ctx,
`INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status) VALUES ($1,$2,TRUE,'active')`,
poolID, workspaceID); err != nil {
t.Fatalf("fixture pool_assignment: %v", err)
}
entID := scan(`INSERT INTO core.numeric_entitlements (pool_id, resource_key, entitlement_type, resource_limit)
VALUES ($1,'fedwiki_sites','limit',64) RETURNING entitlement_id`, poolID)
env := fedwikitest.New(t, database)
tenant := env.Tenant(label)
entitlementID := env.Limit(tenant, 64)
if withCounter {
if _, err := database.ExecContext(ctx,
`INSERT INTO core.numeric_entitlement_usage (entitlement_id, pool_id, resource_key, current_usage)
VALUES ($1,$2,'fedwiki_sites',$3)`, entID, poolID, counter); err != nil {
t.Fatalf("fixture usage: %v", err)
}
env.Usage(tenant, entitlementID, counter)
}
insert := func(n int, status string) {
for i := 0; i < n; i++ {
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1,$2,$3)`,
workspaceID, fmt.Sprintf("%s-%s%d.localtest.me", uniq, status, i), status); err != nil {
t.Fatalf("fixture site: %v", err)
}
}
for i := 0; i < active; i++ {
fedwikitest.Site(t, database, tenant.Workspace.ID, fmt.Sprintf("%s-%s-active%d.localtest.me", label, env.Sfx, i), "active")
}
insert(active, "active")
insert(archived, "archived")
return workspaceID
for i := 0; i < archived; i++ {
fedwikitest.Site(t, database, tenant.Workspace.ID, fmt.Sprintf("%s-%s-archived%d.localtest.me", label, env.Sfx, i), "archived")
}
return tenant.Workspace.ID
}
func counterOf(t *testing.T, database *sql.DB, workspaceID string) int64 {
@@ -15,7 +15,7 @@ import (
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"github.com/alexedwards/scs/v2"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/authtest"
)
// noopEntitlements answers every quota lookup with an error so
@@ -45,13 +45,8 @@ func authedFedWikiHandler(t *testing.T, allowedDomains []string) (*FedWikiPartia
if err != nil {
t.Fatalf("construct handler: %v", err)
}
sm := scs.New()
ctx, err := sm.Load(context.Background(), "")
if err != nil {
t.Fatalf("load session: %v", err)
}
sm.Put(ctx, "authenticated", true)
sm.Put(ctx, "workspace_id", "ws-1")
sm := authtest.NewSessionManager()
ctx := authtest.SignIn(t, sm, auth.SignIn{WorkspaceID: "ws-1"})
h.AuthConfig = &auth.Config{SessionManager: sm}
return h, ctx
}
@@ -18,56 +18,35 @@ import (
"testing"
"time"
"github.com/alexedwards/scs/v2"
"github.com/stretchr/testify/mock"
"go.temporal.io/sdk/mocks"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/server"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/authtest"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/world"
)
// newClaimTestDB opens the test database and seeds the FedWiki provider row.
// newClaimTestDB opens the test database with FedWiki registered:
// domains.placements reference the provider row, which integration
// registration writes at app boot.
func newClaimTestDB(t *testing.T) *sql.DB {
t.Helper()
database := dbtest.Open(t)
// Migrations do not seed the provider registry — integration
// registration does, at app boot. A test database no app has ever
// booted against therefore has no `fedwiki` row, and
// domains.placements FKs onto it, so seed it here rather than
// depending on someone having run the app first.
if _, err := database.ExecContext(context.Background(),
`INSERT INTO core.providers (provider, provider_kind, display_name)
VALUES ($1, 'provisioning', 'FedWiki')
ON CONFLICT (provider) DO NOTHING`, fwmod.ModuleName); err != nil {
t.Fatalf("fixture provider: %v", err)
}
fedwikitest.New(t, database)
return database
}
func newTestWorkspace(t *testing.T, database *sql.DB, label string) string {
t.Helper()
ctx := context.Background()
uniq := fmt.Sprintf("%s-%d", label, time.Now().UnixNano())
scan := func(q string, args ...any) string {
var id string
if err := database.QueryRowContext(ctx, q, args...).Scan(&id); err != nil {
t.Fatalf("fixture %q: %v", q, err)
}
return id
}
userID := scan(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+uniq)
personID := scan(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1,$2,$3) RETURNING person_id`,
userID, "Tester", uniq+"@example.com")
orgID := scan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`,
"Org "+uniq, personID)
return scan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,$2) RETURNING workspace_id`,
orgID, "WS")
w := world.New(t, database)
return w.Workspace(w.Org(label, "personal", w.Person("Tester")), "WS").ID
}
// TestClassifyCustomDomainBranchTable pins the four-branch disposition both
@@ -237,13 +216,8 @@ func (g *gateEntitlements) GetNumericEntitlementByPoolAndResource(context.Contex
// handler and returns the request context carrying it.
func apiSessionFor(t *testing.T, h *FedWikiHandler, workspaceID string) context.Context {
t.Helper()
sm := scs.New()
sctx, err := sm.Load(context.Background(), "")
if err != nil {
t.Fatalf("load session: %v", err)
}
sm.Put(sctx, "authenticated", true)
sm.Put(sctx, "workspace_id", workspaceID)
sm := authtest.NewSessionManager()
sctx := authtest.SignIn(t, sm, auth.SignIn{WorkspaceID: workspaceID})
h.AuthConfig = &auth.Config{SessionManager: sm}
return sctx
}
@@ -405,13 +379,8 @@ func TestCreateFormAffordanceSurvivesRevocation(t *testing.T) {
// gate the registry enforces at ClaimExternal.
h.CustomDomainsGate = server.NewExternalClaimGate(h.EntitlementsQ, h.CustomDomainTarget)
sm := scs.New()
sctx, err := sm.Load(context.Background(), "")
if err != nil {
t.Fatalf("load session: %v", err)
}
sm.Put(sctx, "authenticated", true)
sm.Put(sctx, "workspace_id", tc.workspace)
sm := authtest.NewSessionManager()
sctx := authtest.SignIn(t, sm, auth.SignIn{WorkspaceID: tc.workspace})
h.AuthConfig = &auth.Config{SessionManager: sm}
rec := httptest.NewRecorder()
@@ -14,7 +14,9 @@ import (
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/world"
)
// seedOperatorSites gives one organization `n` sites whose domains all carry
@@ -26,43 +28,21 @@ import (
// last run left behind.
func seedOperatorSites(t *testing.T, database *sql.DB, token string, n int) string {
t.Helper()
ctx := context.Background()
scan := func(q string, args ...any) string {
var id string
if err := database.QueryRowContext(ctx, q, args...).Scan(&id); err != nil {
t.Fatalf("fixture %q: %v", q, err)
}
return id
}
userID := scan(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+token)
personID := scan(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1,$2,$3) RETURNING person_id`,
userID, "Tester", token+"@example.com")
orgID := scan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`,
"Holder "+token, personID)
workspaceID := scan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,'WS') RETURNING workspace_id`, orgID)
w := world.New(t, database)
person := w.Person("Tester")
holder := w.Workspace(w.Org("Holder "+token, "personal", person), "WS")
statuses := []string{"active", "readonly", "archived"}
for i := 0; i < n; i++ {
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1,$2,$3)`,
workspaceID, fmt.Sprintf("s%03d-%s.localtest.me", i, token), statuses[i%len(statuses)]); err != nil {
t.Fatalf("fixture site %d: %v", i, err)
}
fedwikitest.Site(t, database, holder.ID, fmt.Sprintf("s%03d-%s.localtest.me", i, token), statuses[i%len(statuses)])
}
// A site whose domain does NOT carry the token, owned by an
// organization whose name does: the search must reach it through the
// join, which the pre-pagination per-row org lookup could not do.
otherOrg := scan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`,
"Second "+token, personID)
otherWS := scan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,'WS') RETURNING workspace_id`, otherOrg)
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1,$2,'active')`,
otherWS, fmt.Sprintf("byholder-%d.example.test", time.Now().UnixNano())); err != nil {
t.Fatalf("fixture holder-matched site: %v", err)
}
return workspaceID
other := w.Workspace(w.Org("Second "+token, "personal", person), "WS")
fedwikitest.Site(t, database, other.ID, fmt.Sprintf("byholder-%d.example.test", time.Now().UnixNano()), "active")
return holder.ID
}
// sitesPageSize is the operator sites list's page size.
@@ -14,12 +14,12 @@ import (
"strings"
"testing"
"github.com/alexedwards/scs/v2"
"github.com/google/uuid"
"go.temporal.io/sdk/mocks"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/authtest"
)
// oracleSiteQ is a minimal fwmod.Querier (the embedded nil interface
@@ -45,14 +45,8 @@ func (q oracleSiteQ) GetSiteByDomain(_ context.Context, domain string) (fwmod.Ge
// *auth.Config.GetUserSession.
func oracleSession(t *testing.T, workspaceID string) (*auth.Config, context.Context) {
t.Helper()
sm := scs.New()
sctx, err := sm.Load(context.Background(), "")
if err != nil {
t.Fatalf("load session: %v", err)
}
sm.Put(sctx, "authenticated", true)
sm.Put(sctx, "workspace_id", workspaceID)
return &auth.Config{SessionManager: sm}, sctx
sm := authtest.NewSessionManager()
return &auth.Config{SessionManager: sm}, authtest.SignIn(t, sm, auth.SignIn{WorkspaceID: workspaceID})
}
// discardLogger is a *slog.Logger that writes nowhere, for handlers under
@@ -13,13 +13,12 @@ import (
"os"
"strings"
"testing"
"time"
"github.com/alexedwards/scs/v2"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/authtest"
)
// seedUnderstatedQuota gives the workspace `sites` active site rows under a
@@ -28,56 +27,20 @@ import (
// create workflow, so the counter never moved).
func seedUnderstatedQuota(t *testing.T, database *sql.DB, sites int, limit, counter int64) string {
t.Helper()
ctx := context.Background()
uniq := fmt.Sprintf("%d", time.Now().UnixNano())
scan := func(q string, args ...any) string {
var id string
if err := database.QueryRowContext(ctx, q, args...).Scan(&id); err != nil {
t.Fatalf("fixture %q: %v", q, err)
}
return id
}
userID := scan(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+uniq)
personID := scan(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1,$2,$3) RETURNING person_id`,
userID, "Tester", uniq+"@example.com")
orgID := scan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`,
"Org "+uniq, personID)
workspaceID := scan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,'WS') RETURNING workspace_id`, orgID)
poolID := scan(`INSERT INTO core.resource_pools (org_id, name, pool_type) VALUES ($1,'Pool','default') RETURNING pool_id`, orgID)
if _, err := database.ExecContext(ctx,
`INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status) VALUES ($1,$2,TRUE,'active')`,
poolID, workspaceID); err != nil {
t.Fatalf("fixture pool_assignment: %v", err)
}
entID := scan(`INSERT INTO core.numeric_entitlements (pool_id, resource_key, entitlement_type, resource_limit)
VALUES ($1,'fedwiki_sites','limit',$2) RETURNING entitlement_id`, poolID, limit)
if _, err := database.ExecContext(ctx,
`INSERT INTO core.numeric_entitlement_usage (entitlement_id, pool_id, resource_key, current_usage)
VALUES ($1,$2,'fedwiki_sites',$3)`, entID, poolID, counter); err != nil {
t.Fatalf("fixture usage: %v", err)
}
env := fedwikitest.New(t, database)
tenant := env.Counted("Tester", limit, counter)
for i := 0; i < sites; i++ {
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1,$2,'active')`,
workspaceID, fmt.Sprintf("site%d-%s.localtest.me", i, uniq)); err != nil {
t.Fatalf("fixture site %d: %v", i, err)
}
fedwikitest.Site(t, database, tenant.Workspace.ID, fmt.Sprintf("site%d-%s.localtest.me", i, env.Sfx), "active")
}
return workspaceID
return tenant.Workspace.ID
}
// sessionFor returns a request context carrying an authenticated session for
// the workspace, plus the auth config that reads it.
func sessionFor(t *testing.T, workspaceID string) (*auth.Config, context.Context) {
t.Helper()
sm := scs.New()
ctx, err := sm.Load(context.Background(), "")
if err != nil {
t.Fatalf("load session: %v", err)
}
sm.Put(ctx, "authenticated", true)
sm.Put(ctx, "workspace_id", workspaceID)
sm := authtest.NewSessionManager()
ctx := authtest.SignIn(t, sm, auth.SignIn{WorkspaceID: workspaceID})
return &auth.Config{SessionManager: sm}, ctx
}
@@ -27,7 +27,6 @@ import (
"testing"
"time"
"github.com/alexedwards/scs/v2"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client"
@@ -36,9 +35,11 @@ import (
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/workflows"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/authtest"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
// fakeTemporal records every workflow start and answers from a script.
@@ -115,36 +116,13 @@ type siteEnv struct {
func newSiteEnv(t *testing.T, limit int) *siteEnv {
t.Helper()
database := newClaimTestDB(t)
ctx := context.Background()
if err := integration.RegisterProviders(ctx, database, []integration.ProviderSource{fwmod.ProviderSource()}); err != nil {
t.Fatalf("register FedWiki: %v", err)
}
conn, err := integration.New(database).GetConnectionByProvider(ctx, fwmod.ModuleName)
if err != nil {
t.Fatalf("FedWiki connection: %v", err)
}
e := &siteEnv{t: t, ctx: ctx, db: database, conn: conn.ConnectionID, temporal: &fakeTemporal{},
uniq: fmt.Sprintf("%d", time.Now().UnixNano())}
user := e.id(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+e.uniq)
e.person = e.id(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1, 'Requester', $2) RETURNING person_id`,
user, "r-"+e.uniq+"@example.test")
org := e.id(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1, 'personal', $2) RETURNING org_id`, "Org "+e.uniq, e.person)
e.ws = e.id(`INSERT INTO core.workspaces (org_id, name) VALUES ($1, 'WS') RETURNING workspace_id`, org)
e.pool = e.id(`INSERT INTO core.resource_pools (org_id, name, pool_type) VALUES ($1, 'Pool', 'default') RETURNING pool_id`, org)
e.exec(`INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status) VALUES ($1, $2, TRUE, 'active')`, e.pool, e.ws)
e.exec(`INSERT INTO core.numeric_entitlements (pool_id, resource_key, entitlement_type, resource_limit)
VALUES ($1, 'fedwiki_sites', 'limit', $2)`, e.pool, limit)
systemWS, err := systemtenant.Ensure(ctx, database)
if err != nil {
t.Fatal(err)
}
e.root = "web" + e.uniq + ".test"
database := dbtest.Open(t)
env := fedwikitest.New(t, database)
tenant := env.Tenant("Requester")
env.Limit(tenant, int64(limit))
e := &siteEnv{t: t, ctx: env.Ctx, db: database, conn: env.Conn, temporal: &fakeTemporal{}, uniq: env.Sfx,
person: tenant.Person.ID, ws: tenant.Workspace.ID, pool: tenant.Workspace.PoolID, root: env.OperatorRoot("web")}
e.registry = domains.NewRegistry(database)
if _, err := e.registry.EnsureOperatorRoot(ctx, systemWS, e.root); err != nil {
t.Fatal(err)
}
h := newRenderHandler(t)
h.Database = database
@@ -162,22 +140,16 @@ func newSiteEnv(t *testing.T, limit int) *siteEnv {
FedWikiAllowedDomains: []string{e.root}, ConnectionID: func() string { return e.conn },
}
sm := scs.New()
e.sctx, err = sm.Load(context.Background(), "")
if err != nil {
t.Fatal(err)
}
sm.Put(e.sctx, "authenticated", true)
sm.Put(e.sctx, "workspace_id", e.ws)
sm.Put(e.sctx, "person_id", e.person)
sm.Put(e.sctx, "oidc_subject", "sub-"+e.uniq)
sm.Put(e.sctx, "username", "requester")
sm := authtest.NewSessionManager()
signIn := tenant.Person.SignIn()
signIn.WorkspaceID = e.ws
e.sctx = authtest.SignIn(t, sm, signIn)
h.AuthConfig = &auth.Config{SessionManager: sm}
e.api.AuthConfig = h.AuthConfig
t.Cleanup(func() {
_, _ = database.ExecContext(ctx, `DELETE FROM core.lifecycle_requests WHERE workspace_id = $1`, e.ws)
_, _ = database.ExecContext(ctx, `DELETE FROM fedwiki.sites WHERE workspace_id = $1`, e.ws)
_, _ = database.ExecContext(e.ctx, `DELETE FROM core.lifecycle_requests WHERE workspace_id = $1`, e.ws)
_, _ = database.ExecContext(e.ctx, `DELETE FROM fedwiki.sites WHERE workspace_id = $1`, e.ws)
})
return e
}
@@ -202,7 +174,7 @@ func (e *siteEnv) id(q string, args ...any) string {
func (e *siteEnv) site(label, status string) (id, domain string) {
e.t.Helper()
domain = label + "." + e.root
id = e.id(`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1, $2, $3) RETURNING site_id`, e.ws, domain, status)
id = fedwikitest.Site(e.t, e.db, e.ws, domain, status)
return id, domain
}
@@ -4,7 +4,6 @@
package web
import (
"context"
"database/sql"
"encoding/json"
"fmt"
@@ -16,6 +15,7 @@ import (
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/workflows"
)
@@ -25,26 +25,9 @@ import (
// gives it n active sites.
func addSiblingWorkspace(t *testing.T, database *sql.DB, ws string, n int) {
t.Helper()
ctx := context.Background()
var sibling string
if err := database.QueryRowContext(ctx, `
WITH w AS (
INSERT INTO core.workspaces (org_id, name)
SELECT org_id, 'Sibling' FROM core.workspaces WHERE workspace_id = $1
RETURNING workspace_id)
INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status)
SELECT pa.pool_id, w.workspace_id, TRUE, 'active'
FROM core.pool_assignments pa, w
WHERE pa.workspace_id = $1 AND pa.is_primary
RETURNING workspace_id`, ws).Scan(&sibling); err != nil {
t.Fatalf("sibling workspace: %v", err)
}
sibling := fedwikitest.New(t, database).Sibling(ws, "Sibling")
for i := 0; i < n; i++ {
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1, $2, 'active')`,
sibling, fmt.Sprintf("sibling%d-%d.localtest.me", i, time.Now().UnixNano())); err != nil {
t.Fatalf("sibling site: %v", err)
}
fedwikitest.Site(t, database, sibling.ID, fmt.Sprintf("sibling%d-%d.localtest.me", i, time.Now().UnixNano()), "active")
}
}
@@ -16,10 +16,9 @@ import (
"testing"
"time"
"github.com/google/uuid"
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
@@ -52,39 +51,15 @@ func newFakeFarm(t *testing.T) *httptest.Server {
// workspace ID and the three domains oldest-first.
func setupQuotaFixture(t *testing.T, database *sql.DB, limit, usage int) (workspaceID string, oldToNew [3]string) {
t.Helper()
ctx := context.Background()
uniq := fmt.Sprintf("%d", time.Now().UnixNano())
var userID, personID, orgID, poolID, entID string
mustScan := func(q string, args ...any) string {
var id string
if err := database.QueryRowContext(ctx, q, args...).Scan(&id); err != nil {
t.Fatalf("fixture %q: %v", q, err)
}
return id
}
userID = mustScan(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, "sub-"+uniq)
personID = mustScan(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1,$2,$3) RETURNING person_id`, userID, "Tester", "t-"+uniq+"@example.com")
orgID = mustScan(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1,'personal',$2) RETURNING org_id`, "Org "+uniq, personID)
workspaceID = mustScan(`INSERT INTO core.workspaces (org_id, name) VALUES ($1,$2) RETURNING workspace_id`, orgID, "WS")
poolID = mustScan(`INSERT INTO core.resource_pools (org_id, name, pool_type) VALUES ($1,$2,'default') RETURNING pool_id`, orgID, "Pool")
if _, err := database.ExecContext(ctx, `INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status) VALUES ($1,$2,TRUE,'active')`, poolID, workspaceID); err != nil {
t.Fatalf("fixture pool_assignment: %v", err)
}
entID = mustScan(`INSERT INTO core.numeric_entitlements (pool_id, resource_key, entitlement_type, resource_limit) VALUES ($1,'fedwiki_sites','limit',$2) RETURNING entitlement_id`, poolID, limit)
if _, err := database.ExecContext(ctx, `INSERT INTO core.numeric_entitlement_usage (entitlement_id, pool_id, resource_key, current_usage) VALUES ($1,$2,'fedwiki_sites',$3)`, entID, poolID, usage); err != nil {
t.Fatalf("fixture usage: %v", err)
}
env := fedwikitest.New(t, database)
tenant := env.Counted("Tester", int64(limit), int64(usage))
workspaceID = tenant.Workspace.ID
base := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
for i := 0; i < 3; i++ {
domain := fmt.Sprintf("site%d-%s.localtest.me", i, uniq)
domain := fmt.Sprintf("site%d-%s.localtest.me", i, env.Sfx)
oldToNew[i] = domain
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status, last_modified_at) VALUES ($1,$2,'active',$3)`,
workspaceID, domain, base.Add(time.Duration(i)*time.Hour)); err != nil {
t.Fatalf("fixture site %d: %v", i, err)
}
fedwikitest.SiteModifiedAt(t, database, workspaceID, domain, base.Add(time.Duration(i)*time.Hour))
}
return workspaceID, oldToNew
}
@@ -116,6 +91,7 @@ func usageOf(t *testing.T, database *sql.DB, workspaceID string) int64 {
func newReconcileTestEnv(t *testing.T) (*Activities, *sql.DB) {
t.Helper()
database := dbtest.Open(t)
fedwikitest.New(t, database)
farm := newFakeFarm(t)
t.Cleanup(farm.Close)
@@ -137,46 +113,10 @@ func newReconcileTestEnv(t *testing.T) (*Activities, *sql.DB) {
// most-recently-modified active; a re-upgrade reactivates them.
// governFedwikiLimit gives the workspace's pool an active provision of a set
// whose fedwiki_sites rule carries the policy, so the sweep has a governing
// rule to read. The fixture writes rows directly as the test login, which
// migrations grant what the app role is denied.
// rule to read.
func governFedwikiLimit(t *testing.T, database *sql.DB, ws string, policy string) {
t.Helper()
ctx := context.Background()
var poolID, orgID, ownerID string
if err := database.QueryRowContext(ctx,
`SELECT pa.pool_id, rp.org_id, org.owner_person_id FROM core.pool_assignments pa
JOIN core.resource_pools rp ON rp.pool_id = pa.pool_id
JOIN core.organizations org ON org.org_id = rp.org_id
WHERE pa.workspace_id = $1 AND pa.is_primary`, ws).Scan(&poolID, &orgID, &ownerID); err != nil {
t.Fatalf("govern: pool for workspace: %v", err)
}
uniq := uuid.New().String()[:8]
var setID, productID string
if err := database.QueryRowContext(ctx,
`INSERT INTO core.entitlement_sets (name, is_active) VALUES ($1, TRUE) RETURNING set_id`, "Govern "+uniq).Scan(&setID); err != nil {
t.Fatalf("govern: set: %v", err)
}
if _, err := database.ExecContext(ctx,
`INSERT INTO core.entitlement_set_rules (set_id, rule_type, resource_key, resource_value, resource_per_unit, stacking_policy, tier_reduction_policy)
VALUES ($1, 'limit', 'fedwiki_sites', 1, FALSE, 'additive', $2)`, setID, policy); err != nil {
t.Fatalf("govern: rule: %v", err)
}
if err := database.QueryRowContext(ctx,
`INSERT INTO core.products (name, is_active, is_public, lifecycle_status, entitlement_set_id)
VALUES ($1, TRUE, TRUE, 'published', $2) RETURNING product_id`, "Govern "+uniq, setID).Scan(&productID); err != nil {
t.Fatalf("govern: product: %v", err)
}
var grantID string
if err := database.QueryRowContext(ctx,
`INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id, grant_reason, quantity, valid_from)
VALUES ($1, $2, $3, 'manual', 1, NOW()) RETURNING grant_id`, productID, orgID, ownerID).Scan(&grantID); err != nil {
t.Fatalf("govern: grant: %v", err)
}
if _, err := database.ExecContext(ctx,
`INSERT INTO core.pool_provisions (pool_id, product_id, entitlement_set_id, grant_id, status, quantity)
VALUES ($1, $2, $3, $4, 'active', 1)`, poolID, productID, setID, grantID); err != nil {
t.Fatalf("govern: provision: %v", err)
}
fedwikitest.New(t, database).Govern(ws, policy)
}
// Scenario: a clamp policy leaves the surplus alone. The pool is over its
@@ -35,8 +35,8 @@ import (
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
@@ -166,39 +166,12 @@ type reqEnv struct {
func newReqEnv(t *testing.T, limit, usage int) *reqEnv {
t.Helper()
database := dbtest.Open(t)
ctx := context.Background()
if err := integration.RegisterProviders(ctx, database, []integration.ProviderSource{fwmod.ProviderSource()}); err != nil {
t.Fatalf("register FedWiki: %v", err)
}
conn, err := integration.New(database).GetConnectionByProvider(ctx, fwmod.ModuleName)
if err != nil {
t.Fatalf("FedWiki connection: %v", err)
}
env := fedwikitest.New(t, database)
tenant := env.Counted("Requester", int64(limit), int64(usage))
e := &reqEnv{t: t, ctx: ctx, db: database, farm: newLifecycleFarm(t), conn: conn.ConnectionID}
uniq := fmt.Sprintf("%d", time.Now().UnixNano())
e.subject = "subject-" + uniq
user := e.id(`INSERT INTO core.users (oidc_subject) VALUES ($1) RETURNING user_id`, e.subject)
e.person = e.id(`INSERT INTO core.persons (user_id, display_name, primary_email) VALUES ($1, 'Requester', $2) RETURNING person_id`,
user, "r-"+uniq+"@example.test")
org := e.id(`INSERT INTO core.organizations (name, org_type, owner_person_id) VALUES ($1, 'personal', $2) RETURNING org_id`, "Org "+uniq, e.person)
e.ws = e.id(`INSERT INTO core.workspaces (org_id, name) VALUES ($1, 'WS') RETURNING workspace_id`, org)
pool := e.id(`INSERT INTO core.resource_pools (org_id, name, pool_type) VALUES ($1, 'Pool', 'default') RETURNING pool_id`, org)
e.exec(`INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status) VALUES ($1, $2, TRUE, 'active')`, pool, e.ws)
ent := e.id(`INSERT INTO core.numeric_entitlements (pool_id, resource_key, entitlement_type, resource_limit)
VALUES ($1, 'fedwiki_sites', 'limit', $2) RETURNING entitlement_id`, pool, limit)
e.exec(`INSERT INTO core.numeric_entitlement_usage (entitlement_id, pool_id, resource_key, current_usage) VALUES ($1, $2, 'fedwiki_sites', $3)`,
ent, pool, usage)
systemWS, err := systemtenant.Ensure(ctx, database)
if err != nil {
t.Fatal(err)
}
e.root = "req" + uniq + ".test"
e := &reqEnv{t: t, ctx: env.Ctx, db: database, farm: newLifecycleFarm(t), conn: env.Conn,
ws: tenant.Workspace.ID, person: tenant.Person.ID, subject: tenant.Person.Subject, root: env.OperatorRoot("req")}
registry := domains.NewRegistry(database)
if _, err := registry.EnsureOperatorRoot(ctx, systemWS, e.root); err != nil {
t.Fatal(err)
}
e.acts = NewActivities(ActivitiesConfig{
SiteQ: fwmod.New(database),
EntitlementsQ: entitlements.New(database),
@@ -210,7 +183,7 @@ func newReqEnv(t *testing.T, limit, usage int) *reqEnv {
})
t.Cleanup(func() {
for _, inst := range e.requests {
_, _ = database.ExecContext(ctx, `DELETE FROM core.lifecycle_requests WHERE instance_id = $1`, inst)
_, _ = database.ExecContext(e.ctx, `DELETE FROM core.lifecycle_requests WHERE instance_id = $1`, inst)
}
})
return e
@@ -223,15 +196,6 @@ func (e *reqEnv) exec(q string, args ...any) {
}
}
func (e *reqEnv) id(q string, args ...any) string {
e.t.Helper()
var v string
if err := e.db.QueryRowContext(e.ctx, q, args...).Scan(&v); err != nil {
e.t.Fatalf("%s: %v", q, err)
}
return v
}
// record records a request as a handler would and returns its generation.
func (e *reqEnv) record(instance, op, workflowID string, params map[string]any) int64 {
e.t.Helper()
@@ -296,10 +260,9 @@ func (e *reqEnv) placed(fqdn string) bool {
func (e *reqEnv) insertSite(domain, status string, forceReduced bool) string {
e.t.Helper()
id := e.newInstance()
e.exec(`INSERT INTO fedwiki.sites (site_id, workspace_id, domain, status) VALUES ($1, $2, $3, $4)`, id, e.ws, domain, status)
id := fedwikitest.Site(e.t, e.db, e.ws, domain, status)
if forceReduced {
e.exec(`UPDATE fedwiki.sites SET force_reduced_at = NOW() WHERE site_id = $1`, id)
fedwikitest.ForceReduce(e.t, e.db, domain)
}
return id
}
@@ -916,23 +879,14 @@ func TestRecordSwapWritesStatusesWithItsOutcomes_DB(t *testing.T) {
ActivateDomain: activate, ParkDomain: park, FinishedAt: time.Now(),
})
}()
for {
var n int
if err := e.db.QueryRowContext(ctx,
`SELECT count(*) FROM pg_stat_activity WHERE $1 = ANY(pg_blocking_pids(pid))`, holderPID).Scan(&n); err != nil {
t.Fatal(err)
}
if n > 0 {
break
}
dbtest.AwaitLockWait(t, e.db, dbtest.Backend{BlockedBy: holderPID, Abort: func() error {
select {
case err := <-done:
t.Fatalf("the record finished without waiting on the parked request: %v", err)
case <-ctx.Done():
t.Fatal("the record never waited on the parked request")
case <-time.After(5 * time.Millisecond):
return fmt.Errorf("the record finished without waiting on the parked request: %v", err)
default:
return nil
}
}
}})
if status, _ := siteState(t, e.db, park); status != "active" {
t.Errorf("parked site = %s before the outcomes committed, want active", status)
}
@@ -9,6 +9,8 @@ import (
"fmt"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
)
// siblingWorkspace adds a second workspace to ws's organization whose active
@@ -16,28 +18,11 @@ import (
// organization's workspaces draw down one pool) and gives it n active sites.
func siblingWorkspace(t *testing.T, database *sql.DB, ws string, n int, label string) string {
t.Helper()
ctx := context.Background()
var sibling string
if err := database.QueryRowContext(ctx, `
WITH w AS (
INSERT INTO core.workspaces (org_id, name)
SELECT org_id, $2 FROM core.workspaces WHERE workspace_id = $1
RETURNING workspace_id)
INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, status)
SELECT pa.pool_id, w.workspace_id, TRUE, 'active'
FROM core.pool_assignments pa, w
WHERE pa.workspace_id = $1 AND pa.is_primary
RETURNING workspace_id`, ws, label).Scan(&sibling); err != nil {
t.Fatalf("sibling workspace: %v", err)
}
sibling := fedwikitest.New(t, database).Sibling(ws, label)
for i := 0; i < n; i++ {
if _, err := database.ExecContext(ctx,
`INSERT INTO fedwiki.sites (workspace_id, domain, status) VALUES ($1, $2, 'active')`,
sibling, fmt.Sprintf("%s-%d-%d.localtest.me", label, time.Now().UnixNano(), i)); err != nil {
t.Fatalf("sibling site: %v", err)
}
fedwikitest.Site(t, database, sibling.ID, fmt.Sprintf("%s-%d-%d.localtest.me", label, time.Now().UnixNano(), i), "active")
}
return sibling
return sibling.ID
}
// poolActiveSites counts the active sites of every workspace whose active
@@ -5,10 +5,12 @@ package workflows
import (
"context"
"fmt"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
// RecordSwapActivity's outcomes commit with its statuses, not before them:
@@ -51,23 +53,14 @@ func TestRecordSwapCommitsItsOutcomesWithItsStatuses_DB(t *testing.T) {
ActivateDomain: activate, ParkDomain: park, FinishedAt: time.Now(),
})
}()
for {
var n int
if err := e.db.QueryRowContext(ctx,
`SELECT count(*) FROM pg_stat_activity WHERE $1 = ANY(pg_blocking_pids(pid))`, holderPID).Scan(&n); err != nil {
t.Fatal(err)
}
if n > 0 {
break
}
dbtest.AwaitLockWait(t, e.db, dbtest.Backend{BlockedBy: holderPID, Abort: func() error {
select {
case err := <-done:
t.Fatalf("the record finished without writing the activated site's status: %v", err)
case <-ctx.Done():
t.Fatal("the record never waited on the activated site's row")
case <-time.After(5 * time.Millisecond):
return fmt.Errorf("the record finished without writing the activated site's status: %v", err)
default:
return nil
}
}
}})
for _, ref := range []LifecycleRequestRef{{activateID, gen}, {parkID, parkGen}} {
if status, _ := e.request(ref.InstanceID, ref.Generation); status != integration.LifecycleAccepted {
t.Errorf("request on %s = %s while the swap's status write waits, want accepted until the statuses commit with it", ref.InstanceID, status)
@@ -1450,22 +1450,7 @@ func TestInvoiceConvergeKeepsALinkFilledWhileItWaits(t *testing.T) {
// The converge has read the row without the link and waits on the
// fill's row lock.
deadline := time.Now().Add(10 * time.Second)
for {
var waiting int
if err := database.QueryRowContext(ctx,
`SELECT count(*) FROM pg_stat_activity
WHERE wait_event_type = 'Lock' AND pid <> pg_backend_pid() AND query LIKE '-- name: ConvergeInvoice%'`).Scan(&waiting); err != nil {
t.Fatalf("watch the converge: %v", err)
}
if waiting > 0 {
break
}
if time.Now().After(deadline) {
t.Fatal("the converge never waited on the fill's lock")
}
time.Sleep(20 * time.Millisecond)
}
dbtest.AwaitLockWait(t, database, dbtest.Backend{Query: "-- name: ConvergeInvoice"})
if err := fill.Commit(); err != nil {
t.Fatalf("commit the fill: %v", err)
}
+36
View File
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Package authtest builds the signed-in sessions tests hand to handlers. A
// session has the keys and values a completed sign-in writes, because
// SignIn writes them through auth.PutSignIn, the function the callback
// handler calls.
package authtest
import (
"context"
"testing"
"time"
"github.com/alexedwards/scs/v2"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
)
// NewSessionManager returns an in-memory session manager, the one every test
// that signs in builds.
func NewSessionManager() *scs.SessionManager {
return scs.New()
}
// SignIn loads a session on sm, writes s into it as a sign-in does, and
// returns the context that carries it.
func SignIn(tb testing.TB, sm *scs.SessionManager, s auth.SignIn) context.Context {
tb.Helper()
ctx, err := sm.Load(context.Background(), "")
if err != nil {
tb.Fatalf("load session: %v", err)
}
auth.PutSignIn(ctx, sm, s, time.Now())
return ctx
}
+48 -5
View File
@@ -5,6 +5,7 @@ package dbtest
import (
"database/sql"
"fmt"
"testing"
"time"
)
@@ -13,28 +14,70 @@ import (
// test provokes.
const lockWaitDeadline = 10 * time.Second
// Backend selects the server backend a test waits on.
// Backend selects the server backend a test waits on. Exactly one of PID,
// BlockedBy and Query is set.
type Backend struct {
PID int // pg_backend_pid() of the connection that is expected to wait
PID int // pg_backend_pid() of the connection that is expected to wait
BlockedBy int // pg_backend_pid() of the connection whose lock some backend waits on
Query string // text the waiting backend's current statement contains, within this database
// Abort is called between polls. A non-nil error fails the test with its
// text, so a test whose waiting call can return without ever waiting
// reports that instead of polling to the deadline.
Abort func() error
}
// waiting is the statement that counts the backends b selects that wait on a
// lock, and its argument.
func (b Backend) waiting() (string, any) {
switch {
case b.BlockedBy != 0:
return `SELECT count(*) FROM pg_stat_activity WHERE $1 = ANY(pg_blocking_pids(pid))`, b.BlockedBy
case b.Query != "":
return `SELECT count(*) FROM pg_stat_activity
WHERE datname = current_database() AND pid <> pg_backend_pid()
AND wait_event_type = 'Lock' AND position($1 in query) > 0`, b.Query
default:
return `SELECT count(*) FROM pg_stat_activity WHERE pid = $1 AND wait_event_type = 'Lock'`, b.PID
}
}
// AwaitLockWait polls pg_stat_activity until b waits on a lock, so a test can
// assert what the waiting backend holds without sleeping for a fixed time. It
// fails the test when b has not waited after ten seconds.
// fails the test when b has not waited after ten seconds or when b.Abort
// reports an error.
func AwaitLockWait(tb testing.TB, d *sql.DB, b Backend) {
tb.Helper()
query, arg := b.waiting()
deadline := time.Now().Add(lockWaitDeadline)
for {
var waiting int
if err := d.QueryRow(`SELECT count(*) FROM pg_stat_activity WHERE pid = $1 AND wait_event_type = 'Lock'`, b.PID).Scan(&waiting); err != nil {
if err := d.QueryRow(query, arg).Scan(&waiting); err != nil {
tb.Fatalf("read pg_stat_activity: %v", err)
}
if waiting > 0 {
return
}
if b.Abort != nil {
if err := b.Abort(); err != nil {
tb.Fatalf("%v", err)
}
}
if time.Now().After(deadline) {
tb.Fatalf("backend %d never waited on a lock", b.PID)
tb.Fatal(b.neverWaited())
}
time.Sleep(5 * time.Millisecond)
}
}
// neverWaited is the failure message for a selection that no backend matched
// before the deadline.
func (b Backend) neverWaited() string {
switch {
case b.BlockedBy != 0:
return fmt.Sprintf("no backend waited on a lock held by backend %d", b.BlockedBy)
case b.Query != "":
return fmt.Sprintf("no backend running a statement containing %q waited on a lock", b.Query)
default:
return fmt.Sprintf("backend %d never waited on a lock", b.PID)
}
}
+89
View File
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package dbtest
import (
"context"
"errors"
"strings"
"testing"
"time"
)
// holdAndWait holds an advisory lock on one transaction and parks a second
// connection on it, returning both backends' pids and the statement the
// second one runs. The caller rolls the holder back to release the waiter.
func holdAndWait(t *testing.T) (holderPID, waiterPID int, stmt string, release func()) {
t.Helper()
d := Open(t)
ctx := context.Background()
key := time.Now().UnixNano()
stmt = "SELECT pg_advisory_xact_lock($1) /* dbtest wait test */"
holder, err := d.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = holder.Rollback() })
if err := holder.QueryRowContext(ctx, `SELECT pg_backend_pid()`).Scan(&holderPID); err != nil {
t.Fatal(err)
}
if _, err := holder.ExecContext(ctx, stmt, key); err != nil {
t.Fatal(err)
}
waiter, err := d.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = waiter.Rollback() })
if err := waiter.QueryRowContext(ctx, `SELECT pg_backend_pid()`).Scan(&waiterPID); err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() {
_, err := waiter.ExecContext(ctx, stmt, key)
done <- err
}()
return holderPID, waiterPID, stmt, func() {
if err := holder.Rollback(); err != nil {
t.Fatal(err)
}
if err := <-done; err != nil {
t.Fatal(err)
}
}
}
func TestAwaitLockWaitSelectsTheWaitingBackendThreeWays(t *testing.T) {
holderPID, waiterPID, _, release := holdAndWait(t)
defer release()
d := Open(t)
for name, b := range map[string]Backend{
"by pid": {PID: waiterPID},
"by the holder": {BlockedBy: holderPID},
"by statement": {Query: "dbtest wait test"},
} {
t.Run(name, func(t *testing.T) { AwaitLockWait(t, d, b) })
}
}
func TestAwaitLockWaitFailsWithTheAbortProbesText(t *testing.T) {
d := Open(t)
r := &recorder{TB: t}
func() {
defer func() {
if p := recover(); p != nil {
if _, ok := p.(stopped); !ok {
panic(p)
}
}
}()
AwaitLockWait(r, d, Backend{PID: -1, Abort: func() error { return errors.New("the call returned without waiting") }})
}()
if !strings.Contains(r.failure, "the call returned without waiting") {
t.Errorf("Fatalf = %q, want the probe's text", r.failure)
}
}
+52 -9
View File
@@ -5,6 +5,7 @@ package testkit
import (
"os/exec"
"path"
"slices"
"strings"
"testing"
@@ -21,18 +22,46 @@ var allowed = map[string][]string{
"internal/testkit/htmltest": {},
}
// forbidden lists, for each kit package that builds rows through the
// console's own packages, the module packages it must never reach. An entry
// is a path or a path.Match pattern.
var forbidden = map[string][]string{
"internal/testkit/world": {
"internal/migrate", "internal/server", "internal/integrations",
"internal/integrations/*/web", "internal/integrations/*/workflows", "internal/domains",
},
"internal/testkit/authtest": {
"internal/migrate", "internal/server", "internal/integrations",
"internal/integrations/*/web", "internal/integrations/*/workflows", "internal/domains",
},
"internal/integrations/fedwiki/fedwikitest": {
"internal/migrate", "internal/server", "internal/integrations/fedwiki",
"internal/integrations/fedwiki/web", "internal/integrations/fedwiki/workflows",
},
}
// reached returns the module packages pkg depends on, as paths relative to
// the module, without pkg itself.
func reached(t *testing.T, pkg string) []string {
t.Helper()
out, err := exec.Command("go", "list", "-deps", "-f", "{{.ImportPath}}", module+pkg).Output()
if err != nil {
t.Fatalf("go list -deps %s: %v", pkg, err)
}
var deps []string
for _, dep := range strings.Fields(string(out)) {
rel, inModule := strings.CutPrefix(dep, module)
if inModule && rel != pkg {
deps = append(deps, rel)
}
}
return deps
}
func TestKitPackagesReachOnlyTheirAllowedModulePackages(t *testing.T) {
for pkg, ok := range allowed {
t.Run(pkg, func(t *testing.T) {
out, err := exec.Command("go", "list", "-deps", "-f", "{{.ImportPath}}", module+pkg).Output()
if err != nil {
t.Fatalf("go list -deps %s: %v", pkg, err)
}
for _, dep := range strings.Fields(string(out)) {
rel, inModule := strings.CutPrefix(dep, module)
if !inModule || rel == pkg {
continue
}
for _, rel := range reached(t, pkg) {
if !slices.Contains(ok, rel) {
t.Errorf("%s reaches %s, which its row forbids", pkg, rel)
}
@@ -40,3 +69,17 @@ func TestKitPackagesReachOnlyTheirAllowedModulePackages(t *testing.T) {
})
}
}
func TestKitPackagesNeverReachTheirForbiddenModulePackages(t *testing.T) {
for pkg, never := range forbidden {
t.Run(pkg, func(t *testing.T) {
for _, rel := range reached(t, pkg) {
for _, pattern := range never {
if ok, _ := path.Match(pattern, rel); ok {
t.Errorf("%s reaches %s, which its row forbids", pkg, rel)
}
}
}
})
}
}
+261
View File
@@ -0,0 +1,261 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Package world builds the rows a test needs through the functions the console
// itself calls: identity and organization queries, the provisioning step that
// gives a workspace its pool, the catalog queries, the rule-change function
// and the conferral queries. Every method fails the test on any error. It
// suffixes the organization, product and set names, subjects and emails it
// writes with the World's suffix, and deletes nothing. Workspace reads the
// default pool of the organization it is given.
//
// A World holds the testing.TB it was built with, so it belongs to the test
// or subtest that built it.
package world
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"github.com/google/uuid"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
"git.coopcloud.tech/wiki-cafe/member-console/internal/billing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/identity"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
"git.coopcloud.tech/wiki-cafe/member-console/internal/provisioning"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
// World is one test's builder over a database.
type World struct {
TB testing.TB
Ctx context.Context
DB *sql.DB
Sfx string // suffix of every name the World writes
}
// New returns a World over d, any database the console's migrations built.
func New(tb testing.TB, d *sql.DB) *World {
tb.Helper()
return &World{TB: tb, Ctx: context.Background(), DB: d, Sfx: dbtest.Name("")}
}
// Providers registers the providers as boot does and then ensures the System
// tenant, so the provider, resource-key and connection rows exist before a
// fixture names them.
func (w *World) Providers(srcs ...integration.ProviderSource) {
w.TB.Helper()
if err := integration.RegisterProviders(w.Ctx, w.DB, srcs); err != nil {
w.TB.Fatalf("register providers: %v", err)
}
if _, err := systemtenant.Ensure(w.Ctx, w.DB); err != nil {
w.TB.Fatalf("ensure the System tenant: %v", err)
}
}
// Person is an identity: a user and the person it signs in as.
type Person struct {
ID string
UserID string
Subject string
Email string
Name string
}
// SignIn returns the identity a sign-in as p writes into the session.
func (p Person) SignIn() auth.SignIn {
return auth.SignIn{PersonID: p.ID, Subject: p.Subject, Email: p.Email, Name: p.Name, Username: p.Name}
}
// Person creates a user and its person.
func (w *World) Person(name string) Person {
w.TB.Helper()
p := Person{Subject: "sub-" + name + "-" + w.Sfx, Email: strings.ToLower(name) + "-" + w.Sfx + "@example.test", Name: name}
q := identity.New(w.DB)
user, err := q.CreateUser(w.Ctx, p.Subject)
if err != nil {
w.TB.Fatalf("create user %s: %v", name, err)
}
person, err := q.CreatePerson(w.Ctx, identity.CreatePersonParams{
UserID: user.UserID, DisplayName: name, PrimaryEmail: p.Email,
})
if err != nil {
w.TB.Fatalf("create person %s: %v", name, err)
}
p.ID, p.UserID = person.PersonID, user.UserID
return p
}
// Org is an organization with its default resource pool.
type Org struct {
ID string
Name string
PoolID string
Owner Person
}
// Org creates an organization of orgType owned by owner, with its default
// pool, in one materializing transaction. It does not give the organization a
// default plan, so a fixture does not inherit one another package committed.
func (w *World) Org(name, orgType string, owner Person) Org {
w.TB.Helper()
o := Org{Name: name + " " + w.Sfx, Owner: owner}
w.inMaterializingTx("create organization "+name, func(tx *sql.Tx) error {
org, err := organization.New(tx).CreateOrganization(w.Ctx, organization.CreateOrganizationParams{
Name: o.Name, OrgType: orgType, OwnerPersonID: owner.ID,
})
if err != nil {
return err
}
pool, err := entitlements.New(tx).CreateResourcePool(w.Ctx, entitlements.CreateResourcePoolParams{
OrgID: org.OrgID, Name: "Default", PoolType: "default", IsAutoManaged: true,
Key: sql.NullString{String: provisioning.DefaultPoolKey, Valid: true},
})
if err != nil {
return err
}
o.ID, o.PoolID = org.OrgID, pool.PoolID
return nil
})
return o
}
// Workspace is a workspace with its primary assignment to the organization's
// default pool.
type Workspace struct {
ID string
OrgID string
PoolID string
Name string
}
// Workspace creates a workspace in o and assigns it to the default pool, as
// the member's create-workspace handler does.
func (w *World) Workspace(o Org, name string) Workspace {
w.TB.Helper()
ws := Workspace{OrgID: o.ID, Name: name}
w.inMaterializingTx("create workspace "+name, func(tx *sql.Tx) error {
res, err := provisioning.CreateWorkspaceWithPrimaryAssignment(w.Ctx, tx, o.ID, name)
if err != nil {
return err
}
ws.ID, ws.PoolID = res.Workspace.WorkspaceID, res.Pool.PoolID
return nil
})
return ws
}
// Product is a product and the entitlement set it delivers.
type Product struct {
ID string
SetID string
Name string
}
// Product creates a published, public product and its own active set, as the
// operator's create form does.
func (w *World) Product(name string) Product {
w.TB.Helper()
p := Product{Name: name + " " + w.Sfx}
w.inMaterializingTx("create product "+name, func(tx *sql.Tx) error {
set, err := entitlements.New(tx).CreateEntitlementSet(w.Ctx, entitlements.CreateEntitlementSetParams{
Name: p.Name, IsActive: true,
})
if err != nil {
return err
}
product, err := billing.New(tx).CreateProduct(w.Ctx, billing.CreateProductParams{
Name: p.Name, IsActive: true, IsPublic: true, LifecycleStatus: "published",
EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(set.SetID), Valid: true},
})
if err != nil {
return err
}
p.ID, p.SetID = product.ProductID, set.SetID
return nil
})
return p
}
// Rule adds r to p's set through the rule-change function, the only write
// path to a set's rules.
func (w *World) Rule(p Product, r entitlements.RuleFields) {
w.TB.Helper()
tx, err := entitlements.BeginRuleChange(w.Ctx, w.DB)
if err != nil {
w.TB.Fatalf("begin rule change on %s: %v", p.Name, err)
}
defer func() { _ = tx.Rollback() }()
if _, err := entitlements.CommitRuleChangeTx(w.Ctx, entitlements.New(tx), entitlements.CommitRuleChangeInput{
SetID: p.SetID, ChangeKind: entitlements.ChangeKindRuleAdded,
ActorType: entitlements.ActorTypeSystem, Rule: r,
}); err != nil {
w.TB.Fatalf("add a rule to %s: %v", p.Name, err)
}
if err := tx.Commit(); err != nil {
w.TB.Fatalf("commit the rule on %s: %v", p.Name, err)
}
}
// Grant is a conferred grant and the provision it created.
type Grant struct {
ID string
ProvisionID string
}
// Confer records a manual grant of p to o, made by o's owner, and confers it on
// o's default pool. It does not materialize the pool, so the pool keeps the
// rows the test wrote beside it.
func (w *World) Confer(o Org, p Product) Grant {
w.TB.Helper()
var g Grant
w.inMaterializingTx("confer "+p.Name, func(tx *sql.Tx) error {
q := entitlements.New(tx)
grant, err := q.CreateGrant(w.Ctx, entitlements.CreateGrantParams{
ProductID: p.ID,
GrantedToOrgID: uuid.NullUUID{UUID: uuid.MustParse(o.ID), Valid: true},
GrantedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(o.Owner.ID), Valid: true},
GrantReason: "manual",
Quantity: 1,
ValidFrom: time.Now(),
})
if err != nil {
return err
}
provision, _, err := q.Confer(w.Ctx, entitlements.ConferParams{
PoolID: o.PoolID, ProductID: p.ID, GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
Quantity: 1, ActorType: entitlements.ActorTypeSystem,
})
if err != nil {
return err
}
g = Grant{ID: grant.GrantID, ProvisionID: provision}
return nil
})
return g
}
// inMaterializingTx runs fn on a transaction that holds the shared
// materialization rendezvous, commits it, and fails the test naming what on
// any error.
func (w *World) inMaterializingTx(what string, fn func(*sql.Tx) error) {
w.TB.Helper()
tx, err := entitlements.BeginMaterializing(w.Ctx, w.DB)
if err != nil {
w.TB.Fatalf("%s: begin: %v", what, err)
}
defer func() { _ = tx.Rollback() }()
if err := fn(tx); err != nil {
w.TB.Fatalf("%s: %v", what, err)
}
if err := tx.Commit(); err != nil {
w.TB.Fatalf("%s: commit: %v", what, err)
}
}
@@ -11,6 +11,7 @@ import (
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
)
// Two dispatch stamps over the same records lock them in one order whatever
@@ -70,20 +71,6 @@ func TestDispatchStampsLockRecordsInOneOrder(t *testing.T) {
}
return tx, pid
}
waiting := func(pid int) {
for {
var n int
f.scan(&n, `SELECT count(*) FROM pg_stat_activity WHERE pid = $1 AND wait_event_type = 'Lock'`, pid)
if n == 1 {
return
}
if ctx.Err() != nil {
t.Fatal("a stamp never waited")
}
time.Sleep(5 * time.Millisecond)
}
}
holder, _ := begin()
defer holder.Rollback()
if _, err := holder.ExecContext(ctx, `SELECT 1 FROM core.desired_state_records WHERE record_id = $1 FOR UPDATE`, x); err != nil {
@@ -102,9 +89,9 @@ func TestDispatchStampsLockRecordsInOneOrder(t *testing.T) {
byHeap, heapPID := begin("enable_indexscan", "enable_indexonlyscan", "enable_bitmapscan")
indexDone, heapDone := make(chan error, 1), make(chan error, 1)
go stamp(byIndex, indexDone)
waiting(indexPID)
dbtest.AwaitLockWait(t, f.db, dbtest.Backend{PID: indexPID})
go stamp(byHeap, heapDone)
waiting(heapPID)
dbtest.AwaitLockWait(t, f.db, dbtest.Backend{PID: heapPID})
if err := holder.Commit(); err != nil {
t.Fatal(err)
}
+3
View File
@@ -18,6 +18,9 @@ internal/integrations/discourse/discoursetest.* # fake Discourse server (perman
internal/testkit.* # helpers every test package shares (permanent)
internal/testkit/dbtest.* # helpers every database test shares (permanent)
internal/testkit/htmltest.* # HTML parsing and selection every render test shares (permanent)
internal/testkit/world.* # the builder every fixture shares (permanent)
internal/testkit/authtest.* # signed-in sessions every handler test shares (permanent)
internal/integrations/fedwiki/fedwikitest.* # FedWiki's fixtures, shared by its three packages' tests (permanent)
test/e2e/browsertest.* # browser session helpers for the walkthroughs (permanent)
# By design.