@cosmicdrift/kumiko-bundled-features 0.3.0 → 0.4.1
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/CHANGELOG.md +81 -0
- package/package.json +7 -5
- package/src/auth-email-password/i18n.ts +8 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +128 -1
- package/src/auth-email-password/web/login-screen.tsx +73 -8
- package/src/config/__tests__/cascade.integration.ts +419 -0
- package/src/config/__tests__/config.integration.ts +109 -2
- package/src/config/constants.ts +1 -0
- package/src/config/feature.ts +2 -0
- package/src/config/handlers/cascade.query.ts +70 -0
- package/src/config/handlers/values.query.ts +14 -4
- package/src/config/index.ts +17 -0
- package/src/config/resolver.ts +273 -1
- package/src/delivery/__tests__/delivery.integration.ts +6 -0
- package/src/delivery/delivery-service.ts +4 -12
- package/src/delivery/feature.ts +6 -4
- package/src/delivery/index.ts +0 -1
- package/src/legal-pages/web/client-plugin.ts +50 -10
- package/src/renderer-foundation/README.md +86 -0
- package/src/renderer-foundation/__tests__/api.test.ts +188 -0
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +101 -0
- package/src/renderer-foundation/api.ts +106 -0
- package/src/renderer-foundation/constants.ts +21 -0
- package/src/renderer-foundation/feature.ts +47 -0
- package/src/renderer-foundation/index.ts +25 -0
- package/src/renderer-foundation/types.ts +109 -0
- package/src/renderer-simple/__tests__/adapter.test.ts +50 -0
- package/src/renderer-simple/feature.ts +28 -3
- package/src/template-resolver/README.md +89 -0
- package/src/template-resolver/__tests__/handlers.integration.ts +403 -0
- package/src/template-resolver/__tests__/template-resolver.integration.ts +570 -0
- package/src/template-resolver/api.ts +205 -0
- package/src/template-resolver/constants.ts +28 -0
- package/src/template-resolver/feature.ts +36 -0
- package/src/template-resolver/handlers/archive.write.ts +42 -0
- package/src/template-resolver/handlers/find-by-id.query.ts +45 -0
- package/src/template-resolver/handlers/list.query.ts +71 -0
- package/src/template-resolver/handlers/publish.write.ts +45 -0
- package/src/template-resolver/handlers/shared.ts +41 -0
- package/src/template-resolver/handlers/upsert-system.write.ts +81 -0
- package/src/template-resolver/handlers/upsert-tenant.write.ts +105 -0
- package/src/template-resolver/index.ts +28 -0
- package/src/template-resolver/qualified-names.ts +24 -0
- package/src/template-resolver/table.ts +67 -0
- package/src/text-content/__tests__/text-content.integration.ts +54 -0
- package/src/text-content/handlers/by-slug.query.ts +1 -0
- package/src/text-content/handlers/by-tenant.query.ts +2 -0
- package/src/text-content/handlers/set.write.ts +23 -0
- package/src/text-content/seeding.ts +9 -1
- package/src/text-content/table.ts +6 -0
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +125 -0
- package/src/text-content/web/__tests__/group-blocks.test.ts +221 -0
- package/src/text-content/web/client-plugin.tsx +378 -0
- package/src/text-content/web/client-plugin.ts +0 -113
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
setupTestStack,
|
|
4
|
+
type TestStack,
|
|
5
|
+
unsafeCreateEntityTable,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
8
|
+
import { createTemplateResolverApi, TemplateNotFoundError, type TemplateResolverApi } from "../api";
|
|
9
|
+
import {
|
|
10
|
+
type ContentFormat,
|
|
11
|
+
FALLBACK_LOCALE,
|
|
12
|
+
type RenderKind,
|
|
13
|
+
SYSTEM_TENANT_ID,
|
|
14
|
+
type TemplateScope,
|
|
15
|
+
type TemplateStatus,
|
|
16
|
+
} from "../constants";
|
|
17
|
+
import { createTemplateResolverFeature } from "../feature";
|
|
18
|
+
import { templateResourceEntity, templateResourcesTable } from "../table";
|
|
19
|
+
|
|
20
|
+
let stack: TestStack;
|
|
21
|
+
let db: DbConnection;
|
|
22
|
+
let api: TemplateResolverApi;
|
|
23
|
+
|
|
24
|
+
// Fixed UUIDs für reproducierbare Tests. tenantId-Spalte ist UUID-Typ
|
|
25
|
+
// (per buildBaseColumns), String-Sentinels werden von Postgres rejected.
|
|
26
|
+
const TENANT_A = "11111111-1111-4111-8111-111111111111";
|
|
27
|
+
const TENANT_B = "22222222-2222-4222-8222-222222222222";
|
|
28
|
+
|
|
29
|
+
const feature = createTemplateResolverFeature();
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
stack = await setupTestStack({ features: [feature] });
|
|
33
|
+
db = stack.db;
|
|
34
|
+
await unsafeCreateEntityTable(db, templateResourceEntity);
|
|
35
|
+
api = createTemplateResolverApi(db);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
await stack.cleanup();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Direct-DB-Seed-Helper. Umgeht Write-Handlers (kommen in späterem Sprint).
|
|
43
|
+
// Pro Aufruf eine eindeutige (tenantId, slug, kind, locale)-Kombination
|
|
44
|
+
// erwartet — sonst Unique-Constraint-Verletzung.
|
|
45
|
+
async function seedTemplate(args: {
|
|
46
|
+
tenantId: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
kind: RenderKind;
|
|
49
|
+
locale: string;
|
|
50
|
+
scope: TemplateScope;
|
|
51
|
+
status?: TemplateStatus;
|
|
52
|
+
content?: string;
|
|
53
|
+
contentFormat?: ContentFormat;
|
|
54
|
+
variableSchema?: Record<string, unknown>;
|
|
55
|
+
linkedResources?: Record<string, string>;
|
|
56
|
+
parentTemplateId?: string;
|
|
57
|
+
}) {
|
|
58
|
+
await db.insert(templateResourcesTable).values({
|
|
59
|
+
tenantId: args.tenantId,
|
|
60
|
+
slug: args.slug,
|
|
61
|
+
kind: args.kind,
|
|
62
|
+
locale: args.locale,
|
|
63
|
+
scope: args.scope,
|
|
64
|
+
status: args.status ?? "active",
|
|
65
|
+
content: args.content ?? `content for ${args.slug} (${args.locale})`,
|
|
66
|
+
contentFormat: args.contentFormat ?? "markdown",
|
|
67
|
+
variableSchema: JSON.stringify(args.variableSchema ?? {}),
|
|
68
|
+
linkedResources: JSON.stringify(args.linkedResources ?? {}),
|
|
69
|
+
parentTemplateId: args.parentTemplateId ?? null,
|
|
70
|
+
createdBy: "test",
|
|
71
|
+
updatedBy: "test",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("template-resolver :: findExact", () => {
|
|
76
|
+
test("findet existierendes Template", async () => {
|
|
77
|
+
await seedTemplate({
|
|
78
|
+
tenantId: TENANT_A,
|
|
79
|
+
slug: "exact-1",
|
|
80
|
+
kind: "mail-html",
|
|
81
|
+
locale: "de",
|
|
82
|
+
scope: "tenant",
|
|
83
|
+
});
|
|
84
|
+
const result = await api.findExact({
|
|
85
|
+
tenantId: TENANT_A,
|
|
86
|
+
slug: "exact-1",
|
|
87
|
+
kind: "mail-html",
|
|
88
|
+
locale: "de",
|
|
89
|
+
});
|
|
90
|
+
expect(result).not.toBeNull();
|
|
91
|
+
expect(result?.slug).toBe("exact-1");
|
|
92
|
+
expect(result?.locale).toBe("de");
|
|
93
|
+
expect(result?.tenantId).toBe(TENANT_A);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("returnt null wenn Template nicht existiert", async () => {
|
|
97
|
+
const result = await api.findExact({
|
|
98
|
+
tenantId: TENANT_A,
|
|
99
|
+
slug: "does-not-exist",
|
|
100
|
+
kind: "mail-html",
|
|
101
|
+
locale: "de",
|
|
102
|
+
});
|
|
103
|
+
expect(result).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("scope='system' liest aus SYSTEM_TENANT_ID, nicht aus Caller-Tenant", async () => {
|
|
107
|
+
await seedTemplate({
|
|
108
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
109
|
+
slug: "exact-system",
|
|
110
|
+
kind: "notification",
|
|
111
|
+
locale: "de",
|
|
112
|
+
scope: "system",
|
|
113
|
+
});
|
|
114
|
+
const result = await api.findExact({
|
|
115
|
+
tenantId: TENANT_A,
|
|
116
|
+
slug: "exact-system",
|
|
117
|
+
kind: "notification",
|
|
118
|
+
locale: "de",
|
|
119
|
+
scope: "system",
|
|
120
|
+
});
|
|
121
|
+
expect(result).not.toBeNull();
|
|
122
|
+
expect(result?.tenantId).toBe(SYSTEM_TENANT_ID);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("template-resolver :: resolveTemplate :: 4-Stufen-Fallback", () => {
|
|
127
|
+
test("Stufe 1: tenant + requested locale", async () => {
|
|
128
|
+
await seedTemplate({
|
|
129
|
+
tenantId: TENANT_A,
|
|
130
|
+
slug: "fallback-1",
|
|
131
|
+
kind: "mail-html",
|
|
132
|
+
locale: "de",
|
|
133
|
+
scope: "tenant",
|
|
134
|
+
content: "tenant-de-content",
|
|
135
|
+
});
|
|
136
|
+
const result = await api.resolveTemplate({
|
|
137
|
+
tenantId: TENANT_A,
|
|
138
|
+
slug: "fallback-1",
|
|
139
|
+
kind: "mail-html",
|
|
140
|
+
locale: "de",
|
|
141
|
+
});
|
|
142
|
+
expect(result.content).toBe("tenant-de-content");
|
|
143
|
+
expect(result.scope).toBe("tenant");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("Stufe 2: system + requested locale (wenn kein Tenant-Override)", async () => {
|
|
147
|
+
await seedTemplate({
|
|
148
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
149
|
+
slug: "fallback-2",
|
|
150
|
+
kind: "mail-html",
|
|
151
|
+
locale: "tr",
|
|
152
|
+
scope: "system",
|
|
153
|
+
content: "system-tr-content",
|
|
154
|
+
});
|
|
155
|
+
const result = await api.resolveTemplate({
|
|
156
|
+
tenantId: TENANT_A,
|
|
157
|
+
slug: "fallback-2",
|
|
158
|
+
kind: "mail-html",
|
|
159
|
+
locale: "tr",
|
|
160
|
+
});
|
|
161
|
+
expect(result.content).toBe("system-tr-content");
|
|
162
|
+
expect(result.scope).toBe("system");
|
|
163
|
+
expect(result.locale).toBe("tr");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("Stufe 3: tenant + FALLBACK_LOCALE (wenn requested locale fehlt überall)", async () => {
|
|
167
|
+
// Kein tr und kein system-tr — nur tenant-de existiert
|
|
168
|
+
await seedTemplate({
|
|
169
|
+
tenantId: TENANT_A,
|
|
170
|
+
slug: "fallback-3",
|
|
171
|
+
kind: "mail-html",
|
|
172
|
+
locale: FALLBACK_LOCALE,
|
|
173
|
+
scope: "tenant",
|
|
174
|
+
content: "tenant-fallback-content",
|
|
175
|
+
});
|
|
176
|
+
const result = await api.resolveTemplate({
|
|
177
|
+
tenantId: TENANT_A,
|
|
178
|
+
slug: "fallback-3",
|
|
179
|
+
kind: "mail-html",
|
|
180
|
+
locale: "tr",
|
|
181
|
+
});
|
|
182
|
+
expect(result.content).toBe("tenant-fallback-content");
|
|
183
|
+
expect(result.locale).toBe(FALLBACK_LOCALE);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("Stufe 4: system + FALLBACK_LOCALE (letzte Rettung)", async () => {
|
|
187
|
+
await seedTemplate({
|
|
188
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
189
|
+
slug: "fallback-4",
|
|
190
|
+
kind: "mail-html",
|
|
191
|
+
locale: FALLBACK_LOCALE,
|
|
192
|
+
scope: "system",
|
|
193
|
+
content: "system-fallback-content",
|
|
194
|
+
});
|
|
195
|
+
const result = await api.resolveTemplate({
|
|
196
|
+
tenantId: TENANT_A,
|
|
197
|
+
slug: "fallback-4",
|
|
198
|
+
kind: "mail-html",
|
|
199
|
+
locale: "ar",
|
|
200
|
+
});
|
|
201
|
+
expect(result.content).toBe("system-fallback-content");
|
|
202
|
+
expect(result.scope).toBe("system");
|
|
203
|
+
expect(result.locale).toBe(FALLBACK_LOCALE);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("Tenant-Override gewinnt vor System-Default (Stufe 1 vor Stufe 2)", async () => {
|
|
207
|
+
await seedTemplate({
|
|
208
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
209
|
+
slug: "priority-test",
|
|
210
|
+
kind: "mail-html",
|
|
211
|
+
locale: "de",
|
|
212
|
+
scope: "system",
|
|
213
|
+
content: "system-default",
|
|
214
|
+
});
|
|
215
|
+
await seedTemplate({
|
|
216
|
+
tenantId: TENANT_A,
|
|
217
|
+
slug: "priority-test",
|
|
218
|
+
kind: "mail-html",
|
|
219
|
+
locale: "de",
|
|
220
|
+
scope: "tenant",
|
|
221
|
+
content: "tenant-override",
|
|
222
|
+
});
|
|
223
|
+
const result = await api.resolveTemplate({
|
|
224
|
+
tenantId: TENANT_A,
|
|
225
|
+
slug: "priority-test",
|
|
226
|
+
kind: "mail-html",
|
|
227
|
+
locale: "de",
|
|
228
|
+
});
|
|
229
|
+
expect(result.content).toBe("tenant-override");
|
|
230
|
+
expect(result.scope).toBe("tenant");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("Requested-Locale gewinnt vor Fallback-Locale (Stufe 2 vor Stufe 4)", async () => {
|
|
234
|
+
await seedTemplate({
|
|
235
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
236
|
+
slug: "locale-priority",
|
|
237
|
+
kind: "mail-html",
|
|
238
|
+
locale: "tr",
|
|
239
|
+
scope: "system",
|
|
240
|
+
content: "system-tr",
|
|
241
|
+
});
|
|
242
|
+
await seedTemplate({
|
|
243
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
244
|
+
slug: "locale-priority",
|
|
245
|
+
kind: "mail-html",
|
|
246
|
+
locale: FALLBACK_LOCALE,
|
|
247
|
+
scope: "system",
|
|
248
|
+
content: "system-de",
|
|
249
|
+
});
|
|
250
|
+
const result = await api.resolveTemplate({
|
|
251
|
+
tenantId: TENANT_A,
|
|
252
|
+
slug: "locale-priority",
|
|
253
|
+
kind: "mail-html",
|
|
254
|
+
locale: "tr",
|
|
255
|
+
});
|
|
256
|
+
expect(result.content).toBe("system-tr");
|
|
257
|
+
expect(result.locale).toBe("tr");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("wirft TemplateNotFoundError wenn nichts gefunden", async () => {
|
|
261
|
+
await expect(
|
|
262
|
+
api.resolveTemplate({
|
|
263
|
+
tenantId: TENANT_A,
|
|
264
|
+
slug: "completely-missing",
|
|
265
|
+
kind: "mail-html",
|
|
266
|
+
locale: "tr",
|
|
267
|
+
}),
|
|
268
|
+
).rejects.toThrow(TemplateNotFoundError);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("template-resolver :: fallback skips non-active rows", () => {
|
|
273
|
+
test("Stage 1 tenant=draft → Stage 2 system=active wins", async () => {
|
|
274
|
+
await seedTemplate({
|
|
275
|
+
tenantId: TENANT_A,
|
|
276
|
+
slug: "skip-1",
|
|
277
|
+
kind: "mail-html",
|
|
278
|
+
locale: "de",
|
|
279
|
+
scope: "tenant",
|
|
280
|
+
status: "draft",
|
|
281
|
+
content: "tenant-draft",
|
|
282
|
+
});
|
|
283
|
+
await seedTemplate({
|
|
284
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
285
|
+
slug: "skip-1",
|
|
286
|
+
kind: "mail-html",
|
|
287
|
+
locale: "de",
|
|
288
|
+
scope: "system",
|
|
289
|
+
status: "active",
|
|
290
|
+
content: "system-active",
|
|
291
|
+
});
|
|
292
|
+
const result = await api.resolveTemplate({
|
|
293
|
+
tenantId: TENANT_A,
|
|
294
|
+
slug: "skip-1",
|
|
295
|
+
kind: "mail-html",
|
|
296
|
+
locale: "de",
|
|
297
|
+
});
|
|
298
|
+
expect(result.content).toBe("system-active");
|
|
299
|
+
expect(result.scope).toBe("system");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("Stage 1 tenant=archived → Stage 2 system=active wins", async () => {
|
|
303
|
+
await seedTemplate({
|
|
304
|
+
tenantId: TENANT_A,
|
|
305
|
+
slug: "skip-2",
|
|
306
|
+
kind: "mail-html",
|
|
307
|
+
locale: "de",
|
|
308
|
+
scope: "tenant",
|
|
309
|
+
status: "archived",
|
|
310
|
+
});
|
|
311
|
+
await seedTemplate({
|
|
312
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
313
|
+
slug: "skip-2",
|
|
314
|
+
kind: "mail-html",
|
|
315
|
+
locale: "de",
|
|
316
|
+
scope: "system",
|
|
317
|
+
status: "active",
|
|
318
|
+
content: "system-active",
|
|
319
|
+
});
|
|
320
|
+
const result = await api.resolveTemplate({
|
|
321
|
+
tenantId: TENANT_A,
|
|
322
|
+
slug: "skip-2",
|
|
323
|
+
kind: "mail-html",
|
|
324
|
+
locale: "de",
|
|
325
|
+
});
|
|
326
|
+
expect(result.content).toBe("system-active");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("Stages 1+2 inactive → Stage 3 tenant-fallback=active wins", async () => {
|
|
330
|
+
await seedTemplate({
|
|
331
|
+
tenantId: TENANT_A,
|
|
332
|
+
slug: "skip-3",
|
|
333
|
+
kind: "mail-html",
|
|
334
|
+
locale: "tr",
|
|
335
|
+
scope: "tenant",
|
|
336
|
+
status: "draft",
|
|
337
|
+
});
|
|
338
|
+
await seedTemplate({
|
|
339
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
340
|
+
slug: "skip-3",
|
|
341
|
+
kind: "mail-html",
|
|
342
|
+
locale: "tr",
|
|
343
|
+
scope: "system",
|
|
344
|
+
status: "archived",
|
|
345
|
+
});
|
|
346
|
+
await seedTemplate({
|
|
347
|
+
tenantId: TENANT_A,
|
|
348
|
+
slug: "skip-3",
|
|
349
|
+
kind: "mail-html",
|
|
350
|
+
locale: FALLBACK_LOCALE,
|
|
351
|
+
scope: "tenant",
|
|
352
|
+
status: "active",
|
|
353
|
+
content: "tenant-fallback-active",
|
|
354
|
+
});
|
|
355
|
+
const result = await api.resolveTemplate({
|
|
356
|
+
tenantId: TENANT_A,
|
|
357
|
+
slug: "skip-3",
|
|
358
|
+
kind: "mail-html",
|
|
359
|
+
locale: "tr",
|
|
360
|
+
});
|
|
361
|
+
expect(result.content).toBe("tenant-fallback-active");
|
|
362
|
+
expect(result.locale).toBe(FALLBACK_LOCALE);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("alle Stages inactive → throws TemplateNotFoundError", async () => {
|
|
366
|
+
await seedTemplate({
|
|
367
|
+
tenantId: TENANT_A,
|
|
368
|
+
slug: "skip-4",
|
|
369
|
+
kind: "mail-html",
|
|
370
|
+
locale: "de",
|
|
371
|
+
scope: "tenant",
|
|
372
|
+
status: "archived",
|
|
373
|
+
});
|
|
374
|
+
await seedTemplate({
|
|
375
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
376
|
+
slug: "skip-4",
|
|
377
|
+
kind: "mail-html",
|
|
378
|
+
locale: "de",
|
|
379
|
+
scope: "system",
|
|
380
|
+
status: "draft",
|
|
381
|
+
});
|
|
382
|
+
await expect(
|
|
383
|
+
api.resolveTemplate({
|
|
384
|
+
tenantId: TENANT_A,
|
|
385
|
+
slug: "skip-4",
|
|
386
|
+
kind: "mail-html",
|
|
387
|
+
locale: "de",
|
|
388
|
+
}),
|
|
389
|
+
).rejects.toThrow(TemplateNotFoundError);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("template-resolver :: status filtering", () => {
|
|
394
|
+
test("ignoriert status='archived'", async () => {
|
|
395
|
+
await seedTemplate({
|
|
396
|
+
tenantId: TENANT_A,
|
|
397
|
+
slug: "archived-test",
|
|
398
|
+
kind: "mail-html",
|
|
399
|
+
locale: "de",
|
|
400
|
+
scope: "tenant",
|
|
401
|
+
status: "archived",
|
|
402
|
+
content: "archived-content",
|
|
403
|
+
});
|
|
404
|
+
await expect(
|
|
405
|
+
api.resolveTemplate({
|
|
406
|
+
tenantId: TENANT_A,
|
|
407
|
+
slug: "archived-test",
|
|
408
|
+
kind: "mail-html",
|
|
409
|
+
locale: "de",
|
|
410
|
+
}),
|
|
411
|
+
).rejects.toThrow(TemplateNotFoundError);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("ignoriert status='draft'", async () => {
|
|
415
|
+
await seedTemplate({
|
|
416
|
+
tenantId: TENANT_A,
|
|
417
|
+
slug: "draft-test",
|
|
418
|
+
kind: "mail-html",
|
|
419
|
+
locale: "de",
|
|
420
|
+
scope: "tenant",
|
|
421
|
+
status: "draft",
|
|
422
|
+
content: "draft-content",
|
|
423
|
+
});
|
|
424
|
+
await expect(
|
|
425
|
+
api.resolveTemplate({
|
|
426
|
+
tenantId: TENANT_A,
|
|
427
|
+
slug: "draft-test",
|
|
428
|
+
kind: "mail-html",
|
|
429
|
+
locale: "de",
|
|
430
|
+
}),
|
|
431
|
+
).rejects.toThrow(TemplateNotFoundError);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("findet status='active'", async () => {
|
|
435
|
+
await seedTemplate({
|
|
436
|
+
tenantId: TENANT_A,
|
|
437
|
+
slug: "active-test",
|
|
438
|
+
kind: "mail-html",
|
|
439
|
+
locale: "de",
|
|
440
|
+
scope: "tenant",
|
|
441
|
+
status: "active",
|
|
442
|
+
content: "active-content",
|
|
443
|
+
});
|
|
444
|
+
const result = await api.resolveTemplate({
|
|
445
|
+
tenantId: TENANT_A,
|
|
446
|
+
slug: "active-test",
|
|
447
|
+
kind: "mail-html",
|
|
448
|
+
locale: "de",
|
|
449
|
+
});
|
|
450
|
+
expect(result.content).toBe("active-content");
|
|
451
|
+
expect(result.status).toBe("active");
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe("template-resolver :: tenant-isolation", () => {
|
|
456
|
+
test("Tenant B kann Tenant A's Template nicht via resolveTemplate sehen", async () => {
|
|
457
|
+
await seedTemplate({
|
|
458
|
+
tenantId: TENANT_A,
|
|
459
|
+
slug: "isolation-test",
|
|
460
|
+
kind: "mail-html",
|
|
461
|
+
locale: "de",
|
|
462
|
+
scope: "tenant",
|
|
463
|
+
content: "tenant-a-only",
|
|
464
|
+
});
|
|
465
|
+
// Tenant B kennt das Template nicht, fällt durch alle 4 Stufen → TemplateNotFoundError
|
|
466
|
+
await expect(
|
|
467
|
+
api.resolveTemplate({
|
|
468
|
+
tenantId: TENANT_B,
|
|
469
|
+
slug: "isolation-test",
|
|
470
|
+
kind: "mail-html",
|
|
471
|
+
locale: "de",
|
|
472
|
+
}),
|
|
473
|
+
).rejects.toThrow(TemplateNotFoundError);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("Tenant B kann Tenant A's Template nicht via findExact sehen", async () => {
|
|
477
|
+
await seedTemplate({
|
|
478
|
+
tenantId: TENANT_A,
|
|
479
|
+
slug: "isolation-find",
|
|
480
|
+
kind: "mail-html",
|
|
481
|
+
locale: "de",
|
|
482
|
+
scope: "tenant",
|
|
483
|
+
});
|
|
484
|
+
const result = await api.findExact({
|
|
485
|
+
tenantId: TENANT_B,
|
|
486
|
+
slug: "isolation-find",
|
|
487
|
+
kind: "mail-html",
|
|
488
|
+
locale: "de",
|
|
489
|
+
});
|
|
490
|
+
expect(result).toBeNull();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("template-resolver :: JSON-Parsing", () => {
|
|
495
|
+
test("variableSchema + linkedResources werden geparsed", async () => {
|
|
496
|
+
await seedTemplate({
|
|
497
|
+
tenantId: TENANT_A,
|
|
498
|
+
slug: "json-test",
|
|
499
|
+
kind: "mail-html",
|
|
500
|
+
locale: "de",
|
|
501
|
+
scope: "tenant",
|
|
502
|
+
variableSchema: { rentalTenantName: { type: "string", example: "Frau Schmidt" } },
|
|
503
|
+
linkedResources: { logo: "file_abc" },
|
|
504
|
+
});
|
|
505
|
+
const result = await api.resolveTemplate({
|
|
506
|
+
tenantId: TENANT_A,
|
|
507
|
+
slug: "json-test",
|
|
508
|
+
kind: "mail-html",
|
|
509
|
+
locale: "de",
|
|
510
|
+
});
|
|
511
|
+
expect(result.variableSchema).toEqual({
|
|
512
|
+
rentalTenantName: { type: "string", example: "Frau Schmidt" },
|
|
513
|
+
});
|
|
514
|
+
expect(result.linkedResources).toEqual({ logo: "file_abc" });
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("leerer variableSchema + linkedResources sind leere Objekte (nicht undefined)", async () => {
|
|
518
|
+
await seedTemplate({
|
|
519
|
+
tenantId: TENANT_A,
|
|
520
|
+
slug: "json-empty",
|
|
521
|
+
kind: "notification",
|
|
522
|
+
locale: "de",
|
|
523
|
+
scope: "tenant",
|
|
524
|
+
});
|
|
525
|
+
const result = await api.resolveTemplate({
|
|
526
|
+
tenantId: TENANT_A,
|
|
527
|
+
slug: "json-empty",
|
|
528
|
+
kind: "notification",
|
|
529
|
+
locale: "de",
|
|
530
|
+
});
|
|
531
|
+
expect(result.variableSchema).toEqual({});
|
|
532
|
+
expect(result.linkedResources).toEqual({});
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe("template-resolver :: parentTemplateId", () => {
|
|
537
|
+
test("Tenant-Override kann parentTemplateId auf System-Default zeigen", async () => {
|
|
538
|
+
await seedTemplate({
|
|
539
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
540
|
+
slug: "parent-test",
|
|
541
|
+
kind: "mail-html",
|
|
542
|
+
locale: "de",
|
|
543
|
+
scope: "system",
|
|
544
|
+
});
|
|
545
|
+
const systemTemplate = await api.findExact({
|
|
546
|
+
tenantId: TENANT_A,
|
|
547
|
+
slug: "parent-test",
|
|
548
|
+
kind: "mail-html",
|
|
549
|
+
locale: "de",
|
|
550
|
+
scope: "system",
|
|
551
|
+
});
|
|
552
|
+
expect(systemTemplate).not.toBeNull();
|
|
553
|
+
|
|
554
|
+
await seedTemplate({
|
|
555
|
+
tenantId: TENANT_A,
|
|
556
|
+
slug: "parent-test",
|
|
557
|
+
kind: "mail-html",
|
|
558
|
+
locale: "de",
|
|
559
|
+
scope: "tenant",
|
|
560
|
+
parentTemplateId: systemTemplate?.id ?? "",
|
|
561
|
+
});
|
|
562
|
+
const override = await api.findExact({
|
|
563
|
+
tenantId: TENANT_A,
|
|
564
|
+
slug: "parent-test",
|
|
565
|
+
kind: "mail-html",
|
|
566
|
+
locale: "de",
|
|
567
|
+
});
|
|
568
|
+
expect(override?.parentTemplateId).toBe(systemTemplate?.id);
|
|
569
|
+
});
|
|
570
|
+
});
|