//go:build integration package integration import ( "context" "errors" "testing" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/platform/directory" ) // TestTenantDirectoryRoundTrip proves the control plane tenant directory: the MIGRATOR registers a // tenant's database mapping, the APP role (the routing path) reads it, status and migration bookkeeping // update, and an unknown organization is a typed ErrUnknownTenant, never a fallback. This exercises the // 0020 catalog and the directory accessor against the staging database standing in as the control plane. func TestTenantDirectoryRoundTrip(t *testing.T) { ctx := context.Background() org := seedOrg(t) dbName := "tenant_" + org.String()[:8] // unique per org, satisfies the db_name UNIQUE and the name regex // Clean up the directory row so an active entry pointing at a database that this test never created // does not linger and fail a later migrate-all-tenants run. t.Cleanup(func() { _, _ = migrator.Exec(context.Background(), "DELETE FROM tenant_directory WHERE organization_id=$1", org) }) mig := directory.New(migrator) // provisioning writes (migrator role) app := directory.New(appDB.Pool) // routing reads (NOBYPASSRLS app role) if err := mig.Register(ctx, directory.TenantConn{ OrganizationID: org, DBName: dbName, Host: "100.94.8.94", Port: 5432, SecretRef: "tenant-" + org.String()[:8], Status: directory.StatusProvisioning, Cloud: "local", }); err != nil { t.Fatalf("register: %v", err) } // The app role (routing) can read the directory even though it never wrote it. got, err := app.Lookup(ctx, org) if err != nil { t.Fatalf("app lookup: %v", err) } if got.DBName != dbName || got.Host != "100.94.8.94" || got.SecretRef != "tenant-"+org.String()[:8] { t.Fatalf("lookup returned wrong row: %+v", got) } if got.Status != directory.StatusProvisioning { t.Fatalf("status = %q, want provisioning", got.Status) } // Lifecycle: provisioning to active, and migration bookkeeping. if err := mig.SetStatus(ctx, org, directory.StatusActive); err != nil { t.Fatalf("set status: %v", err) } if err := mig.SetMigrated(ctx, org, 20); err != nil { t.Fatalf("set migrated: %v", err) } got, _ = app.Lookup(ctx, org) if got.Status != directory.StatusActive { t.Fatalf("status after activate = %q", got.Status) } if got.LastMigratedVersion == nil || *got.LastMigratedVersion != 20 { t.Fatalf("last_migrated_version = %v, want 20", got.LastMigratedVersion) } // List(active) includes our tenant (the cross tenant migrator iterates this). list, err := app.List(ctx, directory.StatusActive) if err != nil { t.Fatalf("list: %v", err) } found := false for _, tc := range list { if tc.OrganizationID == org { found = true } } if !found { t.Fatal("active list must include the registered tenant") } // An unknown organization is a typed ErrUnknownTenant, never a fallback to another tenant. if _, err := app.Lookup(ctx, uuid.New()); !errors.Is(err, directory.ErrUnknownTenant) { t.Fatalf("unknown org must be ErrUnknownTenant, got %v", err) } }