@cosmicdrift/kumiko-bundled-features 0.2.3 → 0.4.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/package.json +19 -14
  3. package/src/auth-email-password/handlers/change-password.write.ts +1 -1
  4. package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
  5. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
  6. package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
  7. package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
  8. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
  9. package/src/auth-email-password/handlers/login.write.ts +1 -1
  10. package/src/auth-email-password/handlers/logout.write.ts +2 -2
  11. package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  12. package/src/auth-email-password/web/auth-client.ts +1 -1
  13. package/src/billing-foundation/events.ts +1 -1
  14. package/src/billing-foundation/feature.ts +44 -47
  15. package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
  16. package/src/billing-foundation/handlers/process-event.write.ts +3 -3
  17. package/src/billing-foundation/projection.ts +1 -1
  18. package/src/billing-foundation/webhook-handler.ts +1 -1
  19. package/src/cap-counter/constants.ts +1 -1
  20. package/src/cap-counter/enforce-cap.ts +1 -1
  21. package/src/cap-counter/feature.ts +3 -7
  22. package/src/cap-counter/handlers/get-counter.query.ts +1 -1
  23. package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
  24. package/src/cap-counter/handlers/increment.write.ts +3 -3
  25. package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
  26. package/src/channel-email/email-channel.ts +1 -1
  27. package/src/channel-email/types.ts +1 -1
  28. package/src/compliance-profiles/handlers/for-tenant.query.ts +7 -6
  29. package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
  30. package/src/compliance-profiles/handlers/set-profile.write.ts +6 -8
  31. package/src/compliance-profiles/resolve-for-tenant.ts +7 -5
  32. package/src/compliance-profiles/seeding.ts +1 -1
  33. package/src/config/resolver.ts +1 -1
  34. package/src/data-retention/_internal/parse-override.ts +3 -2
  35. package/src/data-retention/handlers/policy-for.query.ts +1 -1
  36. package/src/data-retention/keep-for.ts +1 -1
  37. package/src/data-retention/presets.ts +1 -1
  38. package/src/data-retention/resolve-for-tenant.ts +1 -1
  39. package/src/delivery/__tests__/delivery.integration.ts +6 -0
  40. package/src/delivery/delivery-service.ts +4 -12
  41. package/src/delivery/feature.ts +7 -5
  42. package/src/delivery/index.ts +0 -1
  43. package/src/delivery/testing.ts +1 -2
  44. package/src/delivery/upsert-preference.ts +1 -1
  45. package/src/feature-toggles/feature.ts +1 -1
  46. package/src/feature-toggles/handlers/list.query.ts +1 -1
  47. package/src/feature-toggles/handlers/registered.query.ts +9 -2
  48. package/src/feature-toggles/handlers/set.write.ts +3 -3
  49. package/src/file-foundation/feature.ts +1 -1
  50. package/src/file-provider-s3/feature.ts +2 -2
  51. package/src/files-provider-s3/s3-provider.ts +2 -2
  52. package/src/jobs/handlers/list.query.ts +3 -3
  53. package/src/jobs/handlers/trigger.write.ts +1 -1
  54. package/src/legal-pages/constants.ts +1 -0
  55. package/src/legal-pages/web/client-plugin.ts +82 -0
  56. package/src/legal-pages/web/index.ts +4 -0
  57. package/src/mail-foundation/feature.ts +1 -1
  58. package/src/mail-transport-smtp/feature.ts +2 -2
  59. package/src/renderer-foundation/README.md +86 -0
  60. package/src/renderer-foundation/__tests__/api.test.ts +188 -0
  61. package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +101 -0
  62. package/src/renderer-foundation/api.ts +106 -0
  63. package/src/renderer-foundation/constants.ts +21 -0
  64. package/src/renderer-foundation/feature.ts +47 -0
  65. package/src/renderer-foundation/index.ts +25 -0
  66. package/src/renderer-foundation/types.ts +109 -0
  67. package/src/renderer-simple/__tests__/adapter.test.ts +50 -0
  68. package/src/renderer-simple/feature.ts +28 -3
  69. package/src/renderer-simple/simple-renderer.ts +1 -1
  70. package/src/secrets/handlers/rotate.job.ts +2 -2
  71. package/src/sessions/handlers/cleanup.job.ts +2 -2
  72. package/src/step-dispatcher/feature.ts +62 -0
  73. package/src/step-dispatcher/index.ts +16 -0
  74. package/src/step-dispatcher/mail-runner.ts +32 -0
  75. package/src/step-dispatcher/webhook-runner.ts +67 -0
  76. package/src/subscription-mollie/plugin-methods.ts +1 -1
  77. package/src/subscription-mollie/verify-webhook.ts +9 -5
  78. package/src/subscription-stripe/verify-webhook.ts +3 -3
  79. package/src/template-resolver/README.md +89 -0
  80. package/src/template-resolver/__tests__/handlers.integration.ts +403 -0
  81. package/src/template-resolver/__tests__/template-resolver.integration.ts +570 -0
  82. package/src/template-resolver/api.ts +189 -0
  83. package/src/template-resolver/constants.ts +28 -0
  84. package/src/template-resolver/feature.ts +36 -0
  85. package/src/template-resolver/handlers/archive.write.ts +42 -0
  86. package/src/template-resolver/handlers/find-by-id.query.ts +45 -0
  87. package/src/template-resolver/handlers/list.query.ts +69 -0
  88. package/src/template-resolver/handlers/publish.write.ts +45 -0
  89. package/src/template-resolver/handlers/shared.ts +41 -0
  90. package/src/template-resolver/handlers/upsert-system.write.ts +75 -0
  91. package/src/template-resolver/handlers/upsert-tenant.write.ts +98 -0
  92. package/src/template-resolver/index.ts +28 -0
  93. package/src/template-resolver/qualified-names.ts +24 -0
  94. package/src/template-resolver/table.ts +67 -0
  95. package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  96. package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  97. package/src/tenant/handlers/remove-member.write.ts +1 -1
  98. package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  99. package/src/tenant/handlers/update-member-roles.write.ts +3 -3
  100. package/src/text-content/__tests__/text-content.integration.ts +54 -0
  101. package/src/text-content/constants.ts +2 -0
  102. package/src/text-content/feature.ts +20 -4
  103. package/src/text-content/handlers/by-slug.query.ts +1 -0
  104. package/src/text-content/handlers/by-tenant.query.ts +58 -0
  105. package/src/text-content/handlers/set.write.ts +24 -1
  106. package/src/text-content/seeding.ts +9 -1
  107. package/src/text-content/table.ts +6 -0
  108. package/src/text-content/web/__tests__/editor-read-only.test.tsx +125 -0
  109. package/src/text-content/web/__tests__/group-blocks.test.ts +221 -0
  110. package/src/text-content/web/client-plugin.tsx +378 -0
  111. package/src/text-content/web/index.ts +8 -0
  112. package/src/tier-engine/feature.ts +8 -8
  113. package/src/user/handlers/find-for-auth.query.ts +1 -1
  114. package/src/user/seeding.ts +2 -2
  115. package/src/user-data-rights/feature.ts +4 -3
  116. package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
  117. package/src/user-data-rights/handlers/download-by-job.query.ts +8 -11
  118. package/src/user-data-rights/handlers/download-by-token.query.ts +14 -16
  119. package/src/user-data-rights/handlers/export-status.query.ts +1 -1
  120. package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  121. package/src/user-data-rights/handlers/request-export.write.ts +2 -2
  122. package/src/user-data-rights/run-export-jobs.ts +2 -2
  123. package/src/user-data-rights/run-forget-cleanup.ts +27 -28
  124. package/src/user-data-rights/run-user-export.ts +1 -1
  125. package/src/user-data-rights/token-helpers.ts +2 -2
  126. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +1 -1
  127. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
@@ -0,0 +1,403 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
4
+ import {
5
+ createTestUser,
6
+ setupTestStack,
7
+ type TestStack,
8
+ TestUsers,
9
+ testTenantId,
10
+ unsafeCreateEntityTable,
11
+ } from "@cosmicdrift/kumiko-framework/stack";
12
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
13
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
14
+ import { createTemplateResolverFeature } from "../feature";
15
+ import { TemplateResolverHandlers, TemplateResolverQueries } from "../qualified-names";
16
+ import { templateResourceEntity } from "../table";
17
+
18
+ let stack: TestStack;
19
+ let db: DbConnection;
20
+
21
+ const systemAdmin = TestUsers.systemAdmin;
22
+ // Explizite, distinct tenantIds — createTestUser default-falls auf
23
+ // TestUsers.admin.tenantId (alle User im selben Tenant). Wir testen
24
+ // Tenant-Isolation, deshalb pro Test-User eigener Tenant.
25
+ const tenantA_Admin = createTestUser({
26
+ id: 2,
27
+ roles: ["TenantAdmin"],
28
+ tenantId: testTenantId(10),
29
+ });
30
+ const tenantB_Admin = createTestUser({
31
+ id: 3,
32
+ roles: ["TenantAdmin"],
33
+ tenantId: testTenantId(20),
34
+ });
35
+ const normalUser = createTestUser({ id: 4, tenantId: testTenantId(10) });
36
+
37
+ const feature = createTemplateResolverFeature();
38
+
39
+ beforeAll(async () => {
40
+ stack = await setupTestStack({ features: [feature] });
41
+ db = stack.db;
42
+ await unsafeCreateEntityTable(db, templateResourceEntity);
43
+ await createEventsTable(db);
44
+ });
45
+
46
+ afterAll(async () => {
47
+ await stack.cleanup();
48
+ });
49
+
50
+ const basePayload = {
51
+ slug: "test-slug",
52
+ kind: "mail-html" as const,
53
+ locale: "de",
54
+ content: "Hello {{variables.name}}",
55
+ contentFormat: "markdown" as const,
56
+ variableSchema: { name: { type: "string" } },
57
+ linkedResources: {},
58
+ };
59
+
60
+ // Audit-Trail via event-store-executor: kein dedizierter Test, weil
61
+ // `stack.events.postSave` im aktuellen Test-Stack-Setup nicht aktiv
62
+ // populated wird (Pipeline-Hooks-Wiring unterschiedlich zum Prod-Path).
63
+ // Indirekter Beweis: alle Resolver/Handler-Tests funktionieren — würde
64
+ // der executor nicht in die DB schreiben, würden findById/list/
65
+ // resolveTemplate alles nichts finden. Wenn echtes Audit-Log-Test
66
+ // gebraucht wird: direkt `read_events`-Tabelle abfragen.
67
+ describe("template-resolver :: upsertSystem", () => {
68
+ test("SystemAdmin kann System-Template anlegen", async () => {
69
+ const result = await stack.http.writeOk<Record<string, unknown>>(
70
+ TemplateResolverHandlers.upsertSystem,
71
+ { ...basePayload, slug: "system-new" },
72
+ systemAdmin,
73
+ );
74
+ expect(result).toMatchObject({ slug: "system-new", isNew: true });
75
+ });
76
+
77
+ test("idempotent — zweiter Call updated existing System-Template", async () => {
78
+ await stack.http.writeOk(
79
+ TemplateResolverHandlers.upsertSystem,
80
+ { ...basePayload, slug: "system-idem", content: "v1" },
81
+ systemAdmin,
82
+ );
83
+ const result = await stack.http.writeOk<Record<string, unknown>>(
84
+ TemplateResolverHandlers.upsertSystem,
85
+ { ...basePayload, slug: "system-idem", content: "v2" },
86
+ systemAdmin,
87
+ );
88
+ expect(result).toMatchObject({ slug: "system-idem", isNew: false });
89
+ });
90
+
91
+ test("TenantAdmin denied (access_denied)", async () => {
92
+ const err = await stack.http.writeErr(
93
+ TemplateResolverHandlers.upsertSystem,
94
+ { ...basePayload, slug: "tenant-blocked" },
95
+ tenantA_Admin,
96
+ );
97
+ expectErrorIncludes(err, "access_denied");
98
+ });
99
+
100
+ test("normal User denied", async () => {
101
+ const err = await stack.http.writeErr(
102
+ TemplateResolverHandlers.upsertSystem,
103
+ { ...basePayload, slug: "user-blocked" },
104
+ normalUser,
105
+ );
106
+ expectErrorIncludes(err, "access_denied");
107
+ });
108
+
109
+ test("invalid slug rejected", async () => {
110
+ const err = await stack.http.writeErr(
111
+ TemplateResolverHandlers.upsertSystem,
112
+ { ...basePayload, slug: "Invalid Slug!" },
113
+ systemAdmin,
114
+ );
115
+ expectErrorIncludes(err, "validation_error");
116
+ });
117
+ });
118
+
119
+ describe("template-resolver :: upsertTenant", () => {
120
+ test("TenantAdmin kann Override für eigenen Tenant anlegen", async () => {
121
+ const result = await stack.http.writeOk<Record<string, unknown>>(
122
+ TemplateResolverHandlers.upsertTenant,
123
+ { ...basePayload, slug: "tenant-own" },
124
+ tenantA_Admin,
125
+ );
126
+ expect(result).toMatchObject({ slug: "tenant-own", isNew: true });
127
+ });
128
+
129
+ test("default-status ist draft, explizites active geht auch", async () => {
130
+ await stack.http.writeOk(
131
+ TemplateResolverHandlers.upsertTenant,
132
+ { ...basePayload, slug: "tenant-default-draft" },
133
+ tenantA_Admin,
134
+ );
135
+ const fetched = await stack.http.queryOk<Record<string, unknown>>(
136
+ TemplateResolverQueries.list,
137
+ { kind: "mail-html", locale: "de", includeSystem: false },
138
+ tenantA_Admin,
139
+ );
140
+ const found = (fetched as unknown as Array<{ slug: string; status: string }>).find(
141
+ (r) => r.slug === "tenant-default-draft",
142
+ );
143
+ expect(found?.status).toBe("draft");
144
+
145
+ await stack.http.writeOk(
146
+ TemplateResolverHandlers.upsertTenant,
147
+ { ...basePayload, slug: "tenant-explicit-active", status: "active" },
148
+ tenantA_Admin,
149
+ );
150
+ const fetched2 = await stack.http.queryOk<Record<string, unknown>>(
151
+ TemplateResolverQueries.list,
152
+ { kind: "mail-html", locale: "de", includeSystem: false },
153
+ tenantA_Admin,
154
+ );
155
+ const found2 = (fetched2 as unknown as Array<{ slug: string; status: string }>).find(
156
+ (r) => r.slug === "tenant-explicit-active",
157
+ );
158
+ expect(found2?.status).toBe("active");
159
+ });
160
+
161
+ test("SystemAdmin kann via tenantIdOverride cross-tenant schreiben", async () => {
162
+ const result = await stack.http.writeOk<Record<string, unknown>>(
163
+ TemplateResolverHandlers.upsertTenant,
164
+ { ...basePayload, slug: "system-override", tenantIdOverride: tenantA_Admin.tenantId },
165
+ systemAdmin,
166
+ );
167
+ expect(result).toMatchObject({ slug: "system-override", isNew: true });
168
+ });
169
+
170
+ test("SystemAdmin-Override auf SYSTEM_TENANT_ID → access_denied (use upsertSystem)", async () => {
171
+ const err = await stack.http.writeErr(
172
+ TemplateResolverHandlers.upsertTenant,
173
+ { ...basePayload, slug: "denied-system-override", tenantIdOverride: SYSTEM_TENANT_ID },
174
+ systemAdmin,
175
+ );
176
+ expectErrorIncludes(err, "access_denied");
177
+ });
178
+
179
+ test("TenantAdmin-Override-Versuch → access_denied", async () => {
180
+ const err = await stack.http.writeErr(
181
+ TemplateResolverHandlers.upsertTenant,
182
+ { ...basePayload, slug: "denied-override", tenantIdOverride: tenantB_Admin.tenantId },
183
+ tenantA_Admin,
184
+ );
185
+ expectErrorIncludes(err, "access_denied");
186
+ });
187
+
188
+ test("normal User denied", async () => {
189
+ const err = await stack.http.writeErr(
190
+ TemplateResolverHandlers.upsertTenant,
191
+ { ...basePayload, slug: "user-denied" },
192
+ normalUser,
193
+ );
194
+ expectErrorIncludes(err, "access_denied");
195
+ });
196
+ });
197
+
198
+ describe("template-resolver :: publish + archive", () => {
199
+ test("publish setzt status auf active", async () => {
200
+ const created = await stack.http.writeOk<{ id: string }>(
201
+ TemplateResolverHandlers.upsertTenant,
202
+ { ...basePayload, slug: "publish-test", status: "draft" },
203
+ tenantA_Admin,
204
+ );
205
+ const published = await stack.http.writeOk<Record<string, unknown>>(
206
+ TemplateResolverHandlers.publish,
207
+ { id: created.id },
208
+ tenantA_Admin,
209
+ );
210
+ expect(published).toMatchObject({ status: "active" });
211
+ });
212
+
213
+ test("publish: TenantA kann TenantB's Template nicht publishen (NotFound)", async () => {
214
+ const created = await stack.http.writeOk<{ id: string }>(
215
+ TemplateResolverHandlers.upsertTenant,
216
+ { ...basePayload, slug: "isolation-publish" },
217
+ tenantA_Admin,
218
+ );
219
+ const err = await stack.http.writeErr(
220
+ TemplateResolverHandlers.publish,
221
+ { id: created.id },
222
+ tenantB_Admin,
223
+ );
224
+ expectErrorIncludes(err, "not_found");
225
+ });
226
+
227
+ test("publish: nicht-existierender ID → NotFound", async () => {
228
+ const err = await stack.http.writeErr(
229
+ TemplateResolverHandlers.publish,
230
+ { id: "00000000-0000-4000-8000-000000000999" },
231
+ tenantA_Admin,
232
+ );
233
+ expectErrorIncludes(err, "not_found");
234
+ });
235
+
236
+ test("archive setzt status auf archived", async () => {
237
+ const created = await stack.http.writeOk<{ id: string }>(
238
+ TemplateResolverHandlers.upsertTenant,
239
+ { ...basePayload, slug: "archive-test", status: "active" },
240
+ tenantA_Admin,
241
+ );
242
+ const archived = await stack.http.writeOk<Record<string, unknown>>(
243
+ TemplateResolverHandlers.archive,
244
+ { id: created.id },
245
+ tenantA_Admin,
246
+ );
247
+ expect(archived).toMatchObject({ status: "archived" });
248
+ });
249
+
250
+ test("archive: tenant-isolation (NotFound bei fremdem Tenant)", async () => {
251
+ const created = await stack.http.writeOk<{ id: string }>(
252
+ TemplateResolverHandlers.upsertTenant,
253
+ { ...basePayload, slug: "isolation-archive" },
254
+ tenantA_Admin,
255
+ );
256
+ const err = await stack.http.writeErr(
257
+ TemplateResolverHandlers.archive,
258
+ { id: created.id },
259
+ tenantB_Admin,
260
+ );
261
+ expectErrorIncludes(err, "not_found");
262
+ });
263
+
264
+ // SystemAdmin-Cross-Tenant-Publish/Archive nicht implementiert: ctx.db ist
265
+ // tenant-scoped (createTenantDb in dispatcher). Braucht `tenantIdOverride`
266
+ // im Schema wie upsertTenant. M2-Erweiterung wenn Admin-UI das fordert.
267
+ });
268
+
269
+ describe("template-resolver :: findById query", () => {
270
+ test("findet eigenes Template", async () => {
271
+ const created = await stack.http.writeOk<{ id: string }>(
272
+ TemplateResolverHandlers.upsertTenant,
273
+ { ...basePayload, slug: "find-own" },
274
+ tenantA_Admin,
275
+ );
276
+ const result = await stack.http.queryOk<Record<string, unknown>>(
277
+ TemplateResolverQueries.findById,
278
+ { id: created.id },
279
+ tenantA_Admin,
280
+ );
281
+ expect(result).toMatchObject({ slug: "find-own", scope: "tenant" });
282
+ });
283
+
284
+ test("returnt null bei fremdem Tenant", async () => {
285
+ const created = await stack.http.writeOk<{ id: string }>(
286
+ TemplateResolverHandlers.upsertTenant,
287
+ { ...basePayload, slug: "find-isolation" },
288
+ tenantA_Admin,
289
+ );
290
+ const result = await stack.http.queryOk(
291
+ TemplateResolverQueries.findById,
292
+ { id: created.id },
293
+ tenantB_Admin,
294
+ );
295
+ expect(result).toBeNull();
296
+ });
297
+
298
+ test("System-Templates sind für alle authentifizierten User sichtbar", async () => {
299
+ const created = await stack.http.writeOk<{ id: string }>(
300
+ TemplateResolverHandlers.upsertSystem,
301
+ { ...basePayload, slug: "find-system" },
302
+ systemAdmin,
303
+ );
304
+ const result = await stack.http.queryOk<Record<string, unknown>>(
305
+ TemplateResolverQueries.findById,
306
+ { id: created.id },
307
+ tenantA_Admin,
308
+ );
309
+ expect(result).toMatchObject({ slug: "find-system", scope: "system" });
310
+ expect((result as { tenantId: string }).tenantId).toBe(SYSTEM_TENANT_ID);
311
+ });
312
+
313
+ // SystemAdmin-Cross-Tenant-FindById nicht implementiert — gleicher Grund
314
+ // wie publish/archive. ctx.db tenant-scoped, braucht tenantIdOverride im
315
+ // findById-Schema. M2-Erweiterung.
316
+ });
317
+
318
+ describe("template-resolver :: list query", () => {
319
+ test("includeSystem=true zeigt eigene + system", async () => {
320
+ await stack.http.writeOk(
321
+ TemplateResolverHandlers.upsertSystem,
322
+ { ...basePayload, slug: "list-system-1", locale: "de", kind: "mail-html" },
323
+ systemAdmin,
324
+ );
325
+ await stack.http.writeOk(
326
+ TemplateResolverHandlers.upsertTenant,
327
+ { ...basePayload, slug: "list-tenant-1", locale: "de", kind: "mail-html" },
328
+ tenantA_Admin,
329
+ );
330
+ const result = (await stack.http.queryOk(
331
+ TemplateResolverQueries.list,
332
+ { kind: "mail-html", locale: "de", includeSystem: true },
333
+ tenantA_Admin,
334
+ )) as Array<{ slug: string; scope: string }>;
335
+ const slugs = result.map((r) => r.slug);
336
+ expect(slugs).toContain("list-system-1");
337
+ expect(slugs).toContain("list-tenant-1");
338
+ });
339
+
340
+ test("includeSystem=false zeigt nur eigenen Tenant", async () => {
341
+ await stack.http.writeOk(
342
+ TemplateResolverHandlers.upsertSystem,
343
+ { ...basePayload, slug: "list-system-2", locale: "tr", kind: "notification" },
344
+ systemAdmin,
345
+ );
346
+ await stack.http.writeOk(
347
+ TemplateResolverHandlers.upsertTenant,
348
+ { ...basePayload, slug: "list-tenant-2", locale: "tr", kind: "notification" },
349
+ tenantA_Admin,
350
+ );
351
+ const result = (await stack.http.queryOk(
352
+ TemplateResolverQueries.list,
353
+ { kind: "notification", locale: "tr", includeSystem: false },
354
+ tenantA_Admin,
355
+ )) as Array<{ slug: string; scope: string }>;
356
+ const slugs = result.map((r) => r.slug);
357
+ expect(slugs).toContain("list-tenant-2");
358
+ expect(slugs).not.toContain("list-system-2");
359
+ });
360
+
361
+ test("tenant-isolation: TenantA's templates nicht für TenantB", async () => {
362
+ await stack.http.writeOk(
363
+ TemplateResolverHandlers.upsertTenant,
364
+ { ...basePayload, slug: "list-iso", locale: "fr", kind: "notification" },
365
+ tenantA_Admin,
366
+ );
367
+ const result = (await stack.http.queryOk(
368
+ TemplateResolverQueries.list,
369
+ { kind: "notification", locale: "fr", includeSystem: false },
370
+ tenantB_Admin,
371
+ )) as Array<{ slug: string }>;
372
+ expect(result.map((r) => r.slug)).not.toContain("list-iso");
373
+ });
374
+
375
+ test("status-Filter funktioniert", async () => {
376
+ const draft = await stack.http.writeOk<{ id: string }>(
377
+ TemplateResolverHandlers.upsertTenant,
378
+ { ...basePayload, slug: "filter-draft", locale: "es", kind: "notification", status: "draft" },
379
+ tenantA_Admin,
380
+ );
381
+ await stack.http.writeOk(
382
+ TemplateResolverHandlers.upsertTenant,
383
+ {
384
+ ...basePayload,
385
+ slug: "filter-active",
386
+ locale: "es",
387
+ kind: "notification",
388
+ status: "active",
389
+ },
390
+ tenantA_Admin,
391
+ );
392
+ const drafts = (await stack.http.queryOk(
393
+ TemplateResolverQueries.list,
394
+ { kind: "notification", locale: "es", status: "draft", includeSystem: false },
395
+ tenantA_Admin,
396
+ )) as Array<{ slug: string }>;
397
+ expect(drafts.map((r) => r.slug)).toContain("filter-draft");
398
+ expect(drafts.map((r) => r.slug)).not.toContain("filter-active");
399
+
400
+ // Sicherstellen dass draft existiert
401
+ expect(draft.id).toBeTruthy();
402
+ });
403
+ });