@cosmicdrift/kumiko-bundled-features 0.289.0 → 0.290.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/auth-email-password/__tests__/query-as-member.integration.test.ts +15 -2
- package/src/auth-mfa/__tests__/verify.integration.test.ts +6 -2
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +2 -0
- package/src/billing-foundation/__tests__/subscription-tier-sync.integration.test.ts +2 -0
- package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +8 -1
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +2 -0
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +2 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +2 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +3 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +4 -0
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +108 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.290.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -133,12 +133,12 @@
|
|
|
133
133
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
139
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
140
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
141
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
136
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.290.0",
|
|
137
|
+
"@cosmicdrift/kumiko-framework": "0.290.0",
|
|
138
|
+
"@cosmicdrift/kumiko-headless": "0.290.0",
|
|
139
|
+
"@cosmicdrift/kumiko-renderer": "0.290.0",
|
|
140
|
+
"@cosmicdrift/kumiko-renderer-web": "0.290.0",
|
|
141
|
+
"@cosmicdrift/kumiko-types": "0.290.0",
|
|
142
142
|
"@mollie/api-client": "^4.5.0",
|
|
143
143
|
"@node-rs/argon2": "^2.0.2",
|
|
144
144
|
"@types/mailparser": "^3.4.6",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
],
|
|
168
168
|
"devDependencies": {
|
|
169
169
|
"@testing-library/user-event": "^14.6.1",
|
|
170
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
171
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
170
|
+
"@cosmicdrift/kumiko-locale-de": "0.290.0",
|
|
171
|
+
"@cosmicdrift/kumiko-locale-es": "0.290.0",
|
|
172
172
|
"jsqr": "^1.4.0"
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -563,6 +563,19 @@ async function readAsMemberErr(
|
|
|
563
563
|
);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
// The request-level teardown gate (auth-middleware) answers 410 before a
|
|
567
|
+
// handler runs, so a teardown tenant can only be probed for the dispatcher's
|
|
568
|
+
// own active-membership rejection through the stack's dispatcher.
|
|
569
|
+
async function readAsMemberErrViaDispatcher(userId: string, targetQn: string) {
|
|
570
|
+
const result = await stack.dispatcher.write(
|
|
571
|
+
"queryasmemberprobe:write:read-as-member",
|
|
572
|
+
{ userId, targetQn, payload: {} },
|
|
573
|
+
admin,
|
|
574
|
+
);
|
|
575
|
+
if (result.isSuccess) throw new Error("expected read-as-member to fail");
|
|
576
|
+
return result.error;
|
|
577
|
+
}
|
|
578
|
+
|
|
566
579
|
async function createNote(ownerId: string, body: string): Promise<void> {
|
|
567
580
|
await stack.http.writeOk("queryasmemberprobe:write:note:create", { ownerId, body }, admin);
|
|
568
581
|
}
|
|
@@ -693,7 +706,7 @@ describe("ctx.queryAsMember — AccessDenied, same generic error for every rejec
|
|
|
693
706
|
await addMembership(userId, TENANT_A);
|
|
694
707
|
await setTenantStatus(TENANT_A, "destroying");
|
|
695
708
|
|
|
696
|
-
const err = await
|
|
709
|
+
const err = await readAsMemberErrViaDispatcher(userId, WHOAMI_QN);
|
|
697
710
|
expect(err.code).toBe("access_denied");
|
|
698
711
|
expect(err.message).toBe(GENERIC_MESSAGE);
|
|
699
712
|
expect(err.details).toEqual(GENERIC_DETAILS);
|
|
@@ -707,7 +720,7 @@ describe("ctx.queryAsMember — AccessDenied, same generic error for every rejec
|
|
|
707
720
|
await addMembership(userId, TENANT_A);
|
|
708
721
|
await setTenantStatus(TENANT_A, "destroyRequested");
|
|
709
722
|
|
|
710
|
-
const err = await
|
|
723
|
+
const err = await readAsMemberErrViaDispatcher(userId, WHOAMI_QN);
|
|
711
724
|
expect(err.code).toBe("access_denied");
|
|
712
725
|
expect(err.message).toBe(GENERIC_MESSAGE);
|
|
713
726
|
expect(err.details).toEqual(GENERIC_DETAILS);
|
|
@@ -375,12 +375,16 @@ describe("mfa verify — re-checks account state the way login.write.ts does", (
|
|
|
375
375
|
resetTenantLifecycleGateCacheForTests();
|
|
376
376
|
|
|
377
377
|
try {
|
|
378
|
-
|
|
378
|
+
// Via the dispatcher, not HTTP: the request-level teardown gate answers
|
|
379
|
+
// 410 before /api/write reaches the handler, and this test asserts the
|
|
380
|
+
// handler's own challenge-token gate.
|
|
381
|
+
const result = await stack.dispatcher.write(
|
|
379
382
|
AuthMfaHandlers.verify,
|
|
380
383
|
{ challengeToken, code: currentTotpCode(secret) },
|
|
381
384
|
GUEST,
|
|
382
385
|
);
|
|
383
|
-
|
|
386
|
+
if (result.isSuccess) throw new Error("expected mfa verify to fail");
|
|
387
|
+
expectErrorIncludes(result.error, "invalid_challenge_token");
|
|
384
388
|
} finally {
|
|
385
389
|
await asRawClient(stack.db).unsafe(
|
|
386
390
|
`UPDATE "${tenantTable.tableName}" SET status = $1 WHERE id = $2`,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "../../compliance-profiles";
|
|
45
45
|
import { createConfigFeature } from "../../config";
|
|
46
46
|
import { createTenantFeature } from "../../tenant/feature";
|
|
47
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
47
48
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
48
49
|
import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id";
|
|
49
50
|
import {
|
|
@@ -147,6 +148,7 @@ beforeAll(async () => {
|
|
|
147
148
|
// subscriptionsProjectionTable wird von setupTestStack automatisch
|
|
148
149
|
// gepusht (r.projection mit `table`-Property → auto-push).
|
|
149
150
|
await createEventsTable(db);
|
|
151
|
+
await unsafeCreateEntityTable(db, tenantEntity);
|
|
150
152
|
await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
|
|
151
153
|
// providerCustomerId/providerSubscriptionId are `personal: "tenant"`
|
|
152
154
|
// PII-subject fields — no executor wires the KMS automatically here (raw r.projection,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "../../compliance-profiles";
|
|
31
31
|
import { createConfigFeature } from "../../config";
|
|
32
32
|
import { createTenantFeature } from "../../tenant/feature";
|
|
33
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
33
34
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
34
35
|
import type { TierMap } from "../../tier-engine/compose-app";
|
|
35
36
|
import { TierEngineHandlers } from "../../tier-engine/constants";
|
|
@@ -112,6 +113,7 @@ beforeAll(async () => {
|
|
|
112
113
|
],
|
|
113
114
|
});
|
|
114
115
|
await createEventsTable(stack.db);
|
|
116
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
115
117
|
await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
|
|
116
118
|
await unsafePushTables(stack.db, { tier_assignments: tierAssignmentTable });
|
|
117
119
|
configurePiiSubjectKms(new InMemoryKmsAdapter());
|
|
@@ -10,10 +10,16 @@ import {
|
|
|
10
10
|
createInMemoryFileProvider,
|
|
11
11
|
type InMemoryFileProvider,
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/files";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
setupTestStack,
|
|
15
|
+
type TestStack,
|
|
16
|
+
TestUsers,
|
|
17
|
+
unsafeCreateEntityTable,
|
|
18
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
19
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
15
20
|
import { createConfigFeature } from "../../config";
|
|
16
21
|
import { createTenantFeature } from "../../tenant/feature";
|
|
22
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
17
23
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
18
24
|
import { documentIngestFoundationFeature } from "../feature";
|
|
19
25
|
|
|
@@ -48,6 +54,7 @@ beforeAll(async () => {
|
|
|
48
54
|
],
|
|
49
55
|
files: { storageProvider: provider },
|
|
50
56
|
});
|
|
57
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
51
58
|
});
|
|
52
59
|
|
|
53
60
|
afterAll(async () => {
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { createConfigFeature } from "../../config";
|
|
47
47
|
import { inboundProviderInMemoryFeature } from "../../inbound-provider-inmemory";
|
|
48
48
|
import { createTenantFeature } from "../../tenant/feature";
|
|
49
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
49
50
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
50
51
|
import { inboundMessageAggregateId, mailThreadAggregateId } from "../aggregate-id";
|
|
51
52
|
import { InboundMailFoundationHandlers, InboundMailFoundationQueries } from "../constants";
|
|
@@ -83,6 +84,7 @@ beforeAll(async () => {
|
|
|
83
84
|
});
|
|
84
85
|
db = stack.db;
|
|
85
86
|
await createEventsTable(db);
|
|
87
|
+
await unsafeCreateEntityTable(db, tenantEntity);
|
|
86
88
|
await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
|
|
87
89
|
// Unmanaged direct-write stores — kein r.entity, kein Auto-Push.
|
|
88
90
|
await unsafeCreateEntityTable(db, syncCursorEntity);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { createConfigFeature } from "../../config";
|
|
28
28
|
import { inboundProviderInMemoryFeature } from "../../inbound-provider-inmemory";
|
|
29
29
|
import { createTenantFeature } from "../../tenant/feature";
|
|
30
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
30
31
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
31
32
|
import { inboundMessageAggregateId } from "../aggregate-id";
|
|
32
33
|
import { InboundMailFoundationHandlers } from "../constants";
|
|
@@ -51,6 +52,7 @@ beforeAll(async () => {
|
|
|
51
52
|
});
|
|
52
53
|
db = stack.db;
|
|
53
54
|
await createEventsTable(db);
|
|
55
|
+
await unsafeCreateEntityTable(db, tenantEntity);
|
|
54
56
|
await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
|
|
55
57
|
await unsafeCreateEntityTable(db, syncCursorEntity);
|
|
56
58
|
await unsafeCreateEntityTable(db, seenMessageEntity);
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
seedInboundMessage,
|
|
34
34
|
} from "../../inbound-provider-inmemory/feature";
|
|
35
35
|
import { createTenantFeature } from "../../tenant/feature";
|
|
36
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
36
37
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
37
38
|
import {
|
|
38
39
|
createInboundMailSupervisor,
|
|
@@ -65,6 +66,7 @@ beforeAll(async () => {
|
|
|
65
66
|
});
|
|
66
67
|
db = stack.db;
|
|
67
68
|
await createEventsTable(db);
|
|
69
|
+
await unsafeCreateEntityTable(db, tenantEntity);
|
|
68
70
|
await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
|
|
69
71
|
await unsafeCreateEntityTable(db, syncCursorEntity);
|
|
70
72
|
await unsafeCreateEntityTable(db, seenMessageEntity);
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
setupTestStack,
|
|
34
34
|
type TestStack,
|
|
35
35
|
testTenantId,
|
|
36
|
+
unsafeCreateEntityTable,
|
|
36
37
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
37
38
|
import type {
|
|
38
39
|
Payment as MolliePayment,
|
|
@@ -42,6 +43,7 @@ import { Hono } from "hono";
|
|
|
42
43
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
43
44
|
import { createConfigFeature } from "../../config";
|
|
44
45
|
import { createTenantFeature } from "../../tenant/feature";
|
|
46
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
45
47
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
46
48
|
import { createSubscriptionMollieFeature } from "../feature";
|
|
47
49
|
import type { MollieClientShape } from "../verify-webhook";
|
|
@@ -150,6 +152,7 @@ beforeAll(async () => {
|
|
|
150
152
|
// subscriptionsProjectionTable wird von setupTestStack automatisch
|
|
151
153
|
// gepusht (r.projection mit `table`-Property → auto-push).
|
|
152
154
|
await createEventsTable(db);
|
|
155
|
+
await unsafeCreateEntityTable(db, tenantEntity);
|
|
153
156
|
|
|
154
157
|
webhookApp = new Hono();
|
|
155
158
|
webhookApp.post(
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
setupTestStack,
|
|
30
30
|
type TestStack,
|
|
31
31
|
testTenantId,
|
|
32
|
+
unsafeCreateEntityTable,
|
|
32
33
|
unsafePushTables,
|
|
33
34
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
34
35
|
import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
tenantSecretsTable,
|
|
46
47
|
} from "../../secrets";
|
|
47
48
|
import { createTenantFeature } from "../../tenant/feature";
|
|
49
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
48
50
|
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
49
51
|
import { createSubscriptionStripeFeature } from "../feature";
|
|
50
52
|
|
|
@@ -115,6 +117,7 @@ beforeAll(async () => {
|
|
|
115
117
|
// gepusht (r.projection mit `table`-Property → auto-push). config +
|
|
116
118
|
// secrets brauchen ihre Tabellen explizit.
|
|
117
119
|
await createEventsTable(db);
|
|
120
|
+
await unsafeCreateEntityTable(db, tenantEntity);
|
|
118
121
|
await unsafePushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
|
|
119
122
|
// Standalone-secrets-context (gleiche KEK) zum direkten Seeden +
|
|
120
123
|
// als systemSecrets für die zweite webhook-app.
|
|
@@ -504,6 +507,7 @@ describe("scenario 6: billing-live gate end-to-end (#104)", () => {
|
|
|
504
507
|
}),
|
|
505
508
|
});
|
|
506
509
|
await createEventsTable(gateStack.db);
|
|
510
|
+
await unsafeCreateEntityTable(gateStack.db, tenantEntity);
|
|
507
511
|
await unsafePushTables(gateStack.db, {
|
|
508
512
|
configValuesTable,
|
|
509
513
|
tenant_secrets: tenantSecretsTable,
|
|
@@ -469,3 +469,111 @@ describe("tenant-lifecycle :: sweep isolates one tenant's failure from another's
|
|
|
469
469
|
expect(eventsB.some((e) => e.type.endsWith("tenant-destruction-stage-succeeded"))).toBe(true);
|
|
470
470
|
});
|
|
471
471
|
});
|
|
472
|
+
|
|
473
|
+
// No authConfig here on purpose: buildServer derives the 410 gate from the
|
|
474
|
+
// mounted tenantLifecycleStatus provider. The stacks above keep the explicit
|
|
475
|
+
// auth.resolveTenantLifecycleStatus override covered.
|
|
476
|
+
describe("tenant-lifecycle :: 410 gate derived from the mounted provider", () => {
|
|
477
|
+
let derivedStack: TestStack;
|
|
478
|
+
|
|
479
|
+
beforeAll(async () => {
|
|
480
|
+
const encryption = createTestEnvelopeCipher(randomBytes(32).toString("base64"));
|
|
481
|
+
const resolver = createConfigResolver({ cipher: encryption });
|
|
482
|
+
derivedStack = await setupTestStack({
|
|
483
|
+
features: [
|
|
484
|
+
createConfigFeature(),
|
|
485
|
+
createUserFeature(),
|
|
486
|
+
createTenantFeature(),
|
|
487
|
+
createComplianceProfilesFeature(),
|
|
488
|
+
authFoundationFeature,
|
|
489
|
+
createSessionsFeature(),
|
|
490
|
+
createTenantLifecycleFeature(),
|
|
491
|
+
],
|
|
492
|
+
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
493
|
+
});
|
|
494
|
+
await unsafeCreateEntityTable(derivedStack.db, tenantEntity);
|
|
495
|
+
await unsafeCreateEntityTable(derivedStack.db, userSessionEntity);
|
|
496
|
+
await unsafeCreateEntityTable(derivedStack.db, tenantComplianceProfileEntity);
|
|
497
|
+
await unsafeCreateEntityTable(derivedStack.db, tenantMembershipEntity);
|
|
498
|
+
await createEventsTable(derivedStack.db);
|
|
499
|
+
await unsafePushTables(derivedStack.db, { configValuesTable });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
afterAll(async () => {
|
|
503
|
+
await derivedStack.cleanup();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
beforeEach(async () => {
|
|
507
|
+
derivedStack.events.reset();
|
|
508
|
+
resetTenantLifecycleGateCacheForTests();
|
|
509
|
+
await resetTestTables(derivedStack.db, [
|
|
510
|
+
tenantTable,
|
|
511
|
+
tenantComplianceProfileTable,
|
|
512
|
+
userSessionTable,
|
|
513
|
+
tenantMembershipsTable,
|
|
514
|
+
eventsTable,
|
|
515
|
+
]);
|
|
516
|
+
await derivedStack.http.writeOk(
|
|
517
|
+
TenantHandlers.create,
|
|
518
|
+
{ id: tenantAdmin.tenantId, key: "acme", name: "ACME Corp" },
|
|
519
|
+
TestUsers.systemAdmin,
|
|
520
|
+
);
|
|
521
|
+
await derivedStack.http.writeOk(SET_PROFILE, { profileKey: "eu-dsgvo" }, tenantAdmin);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
async function rejectionCode(res: Response): Promise<string | undefined> {
|
|
525
|
+
const body = (await res.json()) as { error?: { code?: string } };
|
|
526
|
+
return body.error?.code;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
test("active tenant passes the gate", async () => {
|
|
530
|
+
const res = await derivedStack.http.query(TenantQueries.me, {}, tenantAdmin);
|
|
531
|
+
expect(res.status).toBe(200);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("destroyRequested tenant gets 410 tenant_unavailable without any hand-wiring", async () => {
|
|
535
|
+
await derivedStack.http.writeOk(REQUEST, {}, tenantAdmin);
|
|
536
|
+
resetTenantLifecycleGateCacheForTests();
|
|
537
|
+
|
|
538
|
+
const res = await derivedStack.http.query(TenantQueries.me, {}, tenantAdmin);
|
|
539
|
+
expect(res.status).toBe(410);
|
|
540
|
+
expect(await rejectionCode(res)).toBe("tenant_unavailable");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("cancel-destruction stays exempt while destroyRequested", async () => {
|
|
544
|
+
await derivedStack.http.writeOk(REQUEST, {}, tenantAdmin);
|
|
545
|
+
resetTenantLifecycleGateCacheForTests();
|
|
546
|
+
|
|
547
|
+
const cancelled = await derivedStack.http.writeOk<{ status: string }>(CANCEL, {}, tenantAdmin);
|
|
548
|
+
expect(cancelled.status).toBe("active");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("batch mixing cancel-destruction with another write gets 410", async () => {
|
|
552
|
+
await derivedStack.http.writeOk(REQUEST, {}, tenantAdmin);
|
|
553
|
+
resetTenantLifecycleGateCacheForTests();
|
|
554
|
+
|
|
555
|
+
const res = await derivedStack.http.batch(
|
|
556
|
+
[
|
|
557
|
+
{ type: CANCEL, payload: {} },
|
|
558
|
+
{ type: SET_PROFILE, payload: { profileKey: "eu-dsgvo" } },
|
|
559
|
+
],
|
|
560
|
+
tenantAdmin,
|
|
561
|
+
);
|
|
562
|
+
expect(res.status).toBe(410);
|
|
563
|
+
expect(await rejectionCode(res)).toBe("tenant_unavailable");
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("destroying tenant has no cancel exemption", async () => {
|
|
567
|
+
await updateRows(
|
|
568
|
+
derivedStack.db,
|
|
569
|
+
tenantTable,
|
|
570
|
+
{ status: "destroying" },
|
|
571
|
+
{ id: tenantAdmin.tenantId },
|
|
572
|
+
);
|
|
573
|
+
resetTenantLifecycleGateCacheForTests();
|
|
574
|
+
|
|
575
|
+
const res = await derivedStack.http.write(CANCEL, {}, tenantAdmin);
|
|
576
|
+
expect(res.status).toBe(410);
|
|
577
|
+
expect(await rejectionCode(res)).toBe("tenant_unavailable");
|
|
578
|
+
});
|
|
579
|
+
});
|