Files
member-console/internal/server/workspace_create_guard_test.go
T
cgalo5758 ad7a219adf Enforce schema and boot invariants
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.
2026-08-22 18:02:46 -05:00

182 lines
6.3 KiB
Go

package server_test
// Handler tests for the member "create workspace" atomicity guard
// (schema-hardening tasks 2.1 + 2.5): a failure partway through creation
// rolls back the whole transaction and renders an error instead of
// "Workspace created successfully" (the swallowed-error block this replaces
// used to report success even when the pool assignment silently failed).
// DB-backed via TEST_DATABASE_URL (shared testDB helper, see
// operator_plan_ladders_test.go).
import (
"context"
"database/sql"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/alexedwards/scs/v2"
"github.com/google/uuid"
"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/identity"
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
"git.coopcloud.tech/wiki-cafe/member-console/internal/server"
)
// wcSession builds an authenticated member session context for orgID.
func wcSession(t *testing.T, orgID string) (context.Context, *auth.Config) {
t.Helper()
sm := scs.New()
sctx, err := sm.Load(context.Background(), "")
if err != nil {
t.Fatalf("session load: %v", err)
}
sm.Put(sctx, "authenticated", true)
sm.Put(sctx, "org_id", orgID)
return sctx, &auth.Config{SessionManager: sm}
}
// wcOrg creates a committed organization with no resource pool.
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, "wcp-u-"+uuid.NewString())
if err != nil {
t.Fatalf("user: %v", err)
}
person, err := iq.CreatePerson(ctx, identity.CreatePersonParams{
UserID: user.UserID, DisplayName: name,
PrimaryEmail: "wcp-" + 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: "wcp-" + 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
}
func newWcHandler(t *testing.T, database *sql.DB, authCfg *auth.Config) *server.WorkspacePartialsHandler {
t.Helper()
h, err := server.NewWorkspacePartialsHandler(server.WorkspacePartialsConfig{
OrgQ: organization.New(database),
EntitlementsQ: entitlements.New(database),
Database: database,
AuthConfig: authCfg,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
})
if err != nil {
t.Fatalf("NewWorkspacePartialsHandler: %v", err)
}
return h
}
// A failure partway through creation (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 workspace row survives, and the member sees
// an error instead of "Workspace created successfully" (workspace-management
// spec, "Pool step failure fails the whole creation").
func TestCreateWorkspace_FailureRollsBackAndRendersError(t *testing.T) {
database := testDB(t)
orgID := wcOrg(t, database, "WSPartial Fail Org")
// GetDefaultPoolByOrgID only sees pool_type = 'default', so this decoy
// is invisible to resolution but still collides via
// uq_resource_pools_org_id_slug (migration 00001, not the
// concurrently-authored 00010) once the handler falls through to
// CreateResourcePool.
entQ := entitlements.New(database)
if _, err := entQ.CreateResourcePool(context.Background(), entitlements.CreateResourcePoolParams{
OrgID: orgID, Name: "Decoy", Slug: "default", PoolType: "shared", IsAutoManaged: false,
}); err != nil {
t.Fatalf("seed decoy pool: %v", err)
}
sctx, authCfg := wcSession(t, orgID)
h := newWcHandler(t, database, authCfg)
form := url.Values{"name": {"My Workspace"}, "slug": {"default"}}
req := httptest.NewRequestWithContext(sctx, http.MethodPost, "/partials/workspaces", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
h.CreateWorkspace(rec, req)
body := rec.Body.String()
if strings.Contains(body, "Workspace created successfully") {
t.Fatalf("expected the creation to fail, got a success body: %s", body)
}
if strings.Contains(body, "—") {
t.Errorf("member-facing error contains an em dash: %q", body)
}
workspaces, err := organization.New(database).GetWorkspacesByOrgID(context.Background(), 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))
}
}
// Control: an ordinary creation (no poisoned pool) commits both the
// workspace and its primary pool assignment together, and reports success
// (workspace-management spec, "Successful creation yields an entitled
// workspace").
func TestCreateWorkspace_SuccessYieldsPrimaryPoolAssignment(t *testing.T) {
database := testDB(t)
orgID := wcOrg(t, database, "WSPartial Success Org")
sctx, authCfg := wcSession(t, orgID)
h := newWcHandler(t, database, authCfg)
form := url.Values{"name": {"My Workspace"}, "slug": {"my-workspace"}}
req := httptest.NewRequestWithContext(sctx, http.MethodPost, "/partials/workspaces", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
h.CreateWorkspace(rec, req)
body := rec.Body.String()
if !strings.Contains(body, "Workspace created successfully") {
t.Fatalf("expected success, got: %s", body)
}
workspaces, err := organization.New(database).GetWorkspacesByOrgID(context.Background(), orgID)
if err != nil {
t.Fatalf("list workspaces: %v", err)
}
if len(workspaces) != 1 {
t.Fatalf("workspaces = %d, want 1", len(workspaces))
}
assignment, err := entitlements.New(database).GetPrimaryPoolAssignmentByWorkspace(context.Background(), workspaces[0].WorkspaceID)
if err != nil {
t.Fatalf("expected a primary pool assignment: %v", err)
}
if !assignment.IsPrimary {
t.Error("expected the assignment to be primary")
}
}