Enforce 10j's verified gaps (schema-hardening change): - Migration 00010: partial unique indexes for one default pool and one primary assignment per workspace, plus CHECKs pinning pool/provider/subscription vocabularies and provider lifecycle timestamps. - Workspace creation shares a transactional provisioning function; extension validates its target pool; last-tier deletion of a defaulted ladder is guarded; signup completes plan-less on a broken ladder. - Boot asserts integration slug parity and validates declared config enums; Stripe invoice amounts are range-checked; domain cancellation runs a final evidence probe; rule authoring is additive-only.
157 lines
5.3 KiB
Go
157 lines
5.3 KiB
Go
package provisioning_test
|
|
|
|
// Tests for CreateWorkspaceWithPrimaryAssignment (schema-hardening tasks 2.1 +
|
|
// 2.5): the function both AutoProvision and the member "create workspace"
|
|
// handler now share. AutoProvision's existing tests already exercise the
|
|
// "no pool yet" (create) branch end to end; these tests focus on the
|
|
// "pool already exists" (resolve) branch the member handler relies on, and on
|
|
// the atomicity guarantee itself.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"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/organization"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/provisioning"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// wcOrg creates a committed organization (no pool) for these tests.
|
|
func wcOrg(t *testing.T, database *sql.DB, name string) string {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
tx, err := database.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("begin: %v", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
iq := identity.New(tx)
|
|
oq := organization.New(tx)
|
|
user, err := iq.CreateUser(ctx, "wc-u-"+uuid.NewString())
|
|
if err != nil {
|
|
t.Fatalf("user: %v", err)
|
|
}
|
|
person, err := iq.CreatePerson(ctx, identity.CreatePersonParams{
|
|
UserID: user.UserID, DisplayName: name,
|
|
PrimaryEmail: "wc-" + uuid.New().String()[:8] + "@example.com", PrimaryEmailVerified: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("person: %v", err)
|
|
}
|
|
org, err := oq.CreateOrganization(ctx, organization.CreateOrganizationParams{
|
|
Name: name, Slug: "wc-" + uuid.New().String()[:12], OrgType: "personal", OwnerPersonID: person.PersonID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("org: %v", err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
t.Fatalf("commit: %v", err)
|
|
}
|
|
return org.OrgID
|
|
}
|
|
|
|
// A second workspace for the same org resolves the org's existing default
|
|
// pool instead of creating a second one -- the branch AutoProvision itself
|
|
// never exercises (it always provisions a brand-new org) but the member
|
|
// "create workspace" handler relies on every time.
|
|
func TestCreateWorkspaceWithPrimaryAssignment_ResolvesExistingPool(t *testing.T) {
|
|
database := testDB(t)
|
|
ctx := context.Background()
|
|
orgID := wcOrg(t, database, "WC Resolve Org")
|
|
|
|
tx1, err := database.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res1, err := provisioning.CreateWorkspaceWithPrimaryAssignment(ctx, tx1, orgID, "First", "first")
|
|
if err != nil {
|
|
t.Fatalf("first create: %v", err)
|
|
}
|
|
if err := tx1.Commit(); err != nil {
|
|
t.Fatalf("commit first: %v", err)
|
|
}
|
|
if res1.Pool.PoolID == "" {
|
|
t.Fatal("expected a pool to be created for the org's first workspace")
|
|
}
|
|
|
|
tx2, err := database.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res2, err := provisioning.CreateWorkspaceWithPrimaryAssignment(ctx, tx2, orgID, "Second", "second")
|
|
if err != nil {
|
|
t.Fatalf("second create: %v", err)
|
|
}
|
|
if err := tx2.Commit(); err != nil {
|
|
t.Fatalf("commit second: %v", err)
|
|
}
|
|
|
|
if res2.Pool.PoolID != res1.Pool.PoolID {
|
|
t.Errorf("second workspace got pool %s, want the org's existing default pool %s", res2.Pool.PoolID, res1.Pool.PoolID)
|
|
}
|
|
if res2.Workspace.WorkspaceID == res1.Workspace.WorkspaceID {
|
|
t.Fatal("expected two distinct workspaces")
|
|
}
|
|
if !res2.PoolAssignment.IsPrimary {
|
|
t.Error("expected the second assignment to be primary too (each workspace gets its own primary assignment)")
|
|
}
|
|
|
|
entQ := entitlements.New(database)
|
|
pools, err := entQ.GetResourcePoolsByOrgID(ctx, orgID)
|
|
if err != nil {
|
|
t.Fatalf("list pools: %v", err)
|
|
}
|
|
if len(pools) != 1 {
|
|
t.Errorf("org has %d pools after two workspace creations, want exactly 1 (resolution, not a second creation)", len(pools))
|
|
}
|
|
}
|
|
|
|
// A failure partway through (here: the default-pool insert collides with a
|
|
// pre-existing, differently-typed pool at the same org_id+slug) rolls back
|
|
// the whole transaction -- no orphan workspace row survives with no pool
|
|
// assignment.
|
|
func TestCreateWorkspaceWithPrimaryAssignment_FailureRollsBackWorkspaceInsert(t *testing.T) {
|
|
database := testDB(t)
|
|
ctx := context.Background()
|
|
orgID := wcOrg(t, database, "WC Rollback Org")
|
|
|
|
// Poison the default-pool creation branch: GetDefaultPoolByOrgID filters
|
|
// pool_type = 'default', so a pre-existing 'shared' pool at the same
|
|
// org_id+slug is invisible to resolution but still collides on the
|
|
// pre-existing uq_resource_pools_org_id_slug constraint (migration
|
|
// 00001, not the concurrently-authored 00010) when the function falls
|
|
// through to CreateResourcePool.
|
|
entQ := entitlements.New(database)
|
|
if _, err := entQ.CreateResourcePool(ctx, entitlements.CreateResourcePoolParams{
|
|
OrgID: orgID, Name: "Decoy", Slug: "default", PoolType: "shared", IsAutoManaged: false,
|
|
}); err != nil {
|
|
t.Fatalf("seed decoy pool: %v", err)
|
|
}
|
|
|
|
tx, err := database.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = provisioning.CreateWorkspaceWithPrimaryAssignment(ctx, tx, orgID, "Doomed", "default")
|
|
if err == nil {
|
|
t.Fatal("expected the pool-creation collision to fail")
|
|
}
|
|
if rbErr := tx.Rollback(); rbErr != nil {
|
|
t.Fatalf("rollback: %v", rbErr)
|
|
}
|
|
|
|
oq := organization.New(database)
|
|
workspaces, err := oq.GetWorkspacesByOrgID(ctx, orgID)
|
|
if err != nil {
|
|
t.Fatalf("list workspaces: %v", err)
|
|
}
|
|
if len(workspaces) != 0 {
|
|
t.Errorf("workspaces = %d after a failed creation, want 0 (rolled back, not orphaned)", len(workspaces))
|
|
}
|
|
}
|