@cosmicdrift/kumiko-bundled-features 0.2.2 → 0.3.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/CHANGELOG.md +91 -0
- package/package.json +22 -13
- package/src/auth-email-password/auth-user-row.ts +6 -0
- package/src/auth-email-password/constants.ts +11 -0
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
- package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
- package/src/auth-email-password/handlers/login.write.ts +32 -2
- package/src/auth-email-password/handlers/logout.write.ts +2 -2
- package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/auth-email-password/web/auth-client.ts +1 -1
- package/src/billing-foundation/events.ts +1 -1
- package/src/billing-foundation/feature.ts +44 -47
- package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
- package/src/billing-foundation/handlers/process-event.write.ts +3 -3
- package/src/billing-foundation/projection.ts +1 -1
- package/src/billing-foundation/webhook-handler.ts +1 -1
- package/src/cap-counter/constants.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +1 -1
- package/src/cap-counter/feature.ts +3 -7
- package/src/cap-counter/handlers/get-counter.query.ts +1 -1
- package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
- package/src/cap-counter/handlers/increment.write.ts +3 -3
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-email/email-channel.ts +1 -1
- package/src/channel-email/types.ts +1 -1
- package/src/compliance-profiles/README.md +88 -0
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +308 -0
- package/src/compliance-profiles/__tests__/seeding.integration.ts +93 -0
- package/src/compliance-profiles/feature.ts +51 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +64 -0
- package/src/compliance-profiles/handlers/list-profiles.query.ts +44 -0
- package/src/compliance-profiles/handlers/needs-profile.query.ts +56 -0
- package/src/compliance-profiles/handlers/set-profile.write.ts +144 -0
- package/src/compliance-profiles/handlers/sub-processors.query.ts +43 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/resolve-for-tenant.ts +63 -0
- package/src/compliance-profiles/schema/profile-selection.ts +52 -0
- package/src/compliance-profiles/seeding.ts +96 -0
- package/src/config/resolver.ts +1 -1
- package/src/data-retention/__tests__/data-retention.integration.ts +49 -0
- package/src/data-retention/__tests__/keep-for.test.ts +77 -0
- package/src/data-retention/__tests__/override-schema.test.ts +96 -0
- package/src/data-retention/__tests__/policy-for.integration.ts +172 -0
- package/src/data-retention/__tests__/resolver.test.ts +201 -0
- package/src/data-retention/_internal/parse-override.ts +34 -0
- package/src/data-retention/feature.ts +57 -0
- package/src/data-retention/handlers/policy-for.query.ts +57 -0
- package/src/data-retention/index.ts +18 -0
- package/src/data-retention/keep-for.ts +75 -0
- package/src/data-retention/override-schema.ts +37 -0
- package/src/data-retention/presets.ts +72 -0
- package/src/data-retention/resolve-for-tenant.ts +50 -0
- package/src/data-retention/resolver.ts +107 -0
- package/src/data-retention/schema/tenant-retention-override.ts +47 -0
- package/src/delivery/feature.ts +1 -1
- package/src/delivery/testing.ts +1 -2
- package/src/delivery/upsert-preference.ts +1 -1
- package/src/feature-toggles/feature.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +9 -2
- package/src/feature-toggles/handlers/set.write.ts +3 -3
- package/src/file-foundation/feature.ts +44 -4
- package/src/file-foundation/index.ts +1 -0
- package/src/file-provider-inmemory/feature.ts +6 -3
- package/src/file-provider-s3/feature.ts +10 -12
- package/src/files/README.md +50 -0
- package/src/files/__tests__/files.integration.ts +157 -0
- package/src/files/feature.ts +34 -0
- package/src/files/index.ts +1 -0
- package/src/files/schema/file-ref.ts +58 -0
- package/src/files-provider-s3/s3-provider.ts +90 -1
- package/src/jobs/handlers/list.query.ts +3 -3
- package/src/jobs/handlers/trigger.write.ts +1 -1
- package/src/legal-pages/constants.ts +1 -0
- package/src/legal-pages/web/client-plugin.ts +42 -0
- package/src/legal-pages/web/index.ts +4 -0
- package/src/mail-foundation/feature.ts +1 -1
- package/src/mail-transport-smtp/feature.ts +2 -2
- package/src/renderer-simple/simple-renderer.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +81 -0
- package/src/secrets/feature.ts +10 -6
- package/src/secrets/handlers/rotate.job.ts +2 -2
- package/src/sessions/constants.ts +4 -0
- package/src/sessions/feature.ts +3 -0
- package/src/sessions/handlers/cleanup.job.ts +2 -2
- package/src/sessions/handlers/revoke-all-for-user.write.ts +42 -0
- package/src/step-dispatcher/feature.ts +62 -0
- package/src/step-dispatcher/index.ts +16 -0
- package/src/step-dispatcher/mail-runner.ts +32 -0
- package/src/step-dispatcher/webhook-runner.ts +67 -0
- package/src/subscription-mollie/plugin-methods.ts +1 -1
- package/src/subscription-mollie/verify-webhook.ts +9 -5
- package/src/subscription-stripe/verify-webhook.ts +3 -3
- package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/src/tenant/handlers/remove-member.write.ts +1 -1
- package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/src/tenant/handlers/update-member-roles.write.ts +3 -3
- package/src/text-content/constants.ts +2 -0
- package/src/text-content/feature.ts +20 -4
- package/src/text-content/handlers/by-tenant.query.ts +56 -0
- package/src/text-content/handlers/set.write.ts +1 -1
- package/src/text-content/web/client-plugin.ts +113 -0
- package/src/text-content/web/index.ts +8 -0
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +118 -0
- package/src/tier-engine/feature.ts +23 -13
- package/src/user/__tests__/user-status.test.ts +39 -0
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/index.ts +11 -1
- package/src/user/schema/user.ts +76 -0
- package/src/user/seeding.ts +2 -2
- package/src/user-data-rights/COMPLIANCE.md +182 -0
- package/src/user-data-rights/README.md +109 -0
- package/src/user-data-rights/__tests__/audit-log.integration.ts +199 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +349 -0
- package/src/user-data-rights/__tests__/download.integration.ts +565 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +244 -0
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +163 -0
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +30 -0
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +370 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +179 -0
- package/src/user-data-rights/__tests__/request-export.integration.ts +269 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +309 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +1124 -0
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +703 -0
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +291 -0
- package/src/user-data-rights/__tests__/token-helpers.test.ts +63 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +57 -0
- package/src/user-data-rights/__tests__/zip-path.test.ts +119 -0
- package/src/user-data-rights/audit-download.ts +125 -0
- package/src/user-data-rights/feature.ts +310 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +84 -0
- package/src/user-data-rights/handlers/download-by-job.query.ts +206 -0
- package/src/user-data-rights/handlers/download-by-token.query.ts +255 -0
- package/src/user-data-rights/handlers/export-status.query.ts +76 -0
- package/src/user-data-rights/handlers/lift-restriction.write.ts +68 -0
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +53 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +63 -0
- package/src/user-data-rights/handlers/request-deletion.write.ts +123 -0
- package/src/user-data-rights/handlers/request-export.write.ts +155 -0
- package/src/user-data-rights/handlers/restrict-account.write.ts +81 -0
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +61 -0
- package/src/user-data-rights/i18n.ts +37 -0
- package/src/user-data-rights/index.ts +19 -0
- package/src/user-data-rights/run-export-jobs.ts +878 -0
- package/src/user-data-rights/run-forget-cleanup.ts +333 -0
- package/src/user-data-rights/run-user-export.ts +211 -0
- package/src/user-data-rights/schema/download-attempt.ts +37 -0
- package/src/user-data-rights/schema/download-token.ts +111 -0
- package/src/user-data-rights/schema/export-job.ts +166 -0
- package/src/user-data-rights/token-helpers.ts +67 -0
- package/src/user-data-rights/zip-path.ts +94 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +337 -0
- package/src/user-data-rights-defaults/feature.ts +40 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +109 -0
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +91 -0
- package/src/user-data-rights-defaults/index.ts +6 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// ExportJob Partial-UNIQUE-Index Integration-Test (S2.U3 Atom 1b).
|
|
2
|
+
//
|
|
3
|
+
// Pinst die DB-Constraint die Atom-2's request-export-Handler nutzt:
|
|
4
|
+
// `UNIQUE(userId) WHERE status IN ('pending', 'running')`. Pro User
|
|
5
|
+
// kann es maximal EINEN aktiven Job geben — done/failed-Historie ist
|
|
6
|
+
// unbeschraenkt.
|
|
7
|
+
//
|
|
8
|
+
// Plus DB-Roundtrip-Test fuer bigInt-Spalte (bytesWritten >2^31), der
|
|
9
|
+
// in Atom 1a's pure unit-Test absichtlich ausgelassen wurde weil er
|
|
10
|
+
// reale Postgres + Drizzle-customType-Codec-Path braucht.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
setupTestStack,
|
|
14
|
+
type TestStack,
|
|
15
|
+
unsafeCreateEntityTable,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
17
|
+
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
18
|
+
import { eq, sql } from "drizzle-orm";
|
|
19
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
20
|
+
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
21
|
+
import { createDataRetentionFeature } from "../../data-retention";
|
|
22
|
+
import { createUserFeature } from "../../user";
|
|
23
|
+
import { createUserDataRightsFeature } from "../feature";
|
|
24
|
+
import {
|
|
25
|
+
ACTIVE_JOB_CONSTRAINT,
|
|
26
|
+
EXPORT_JOB_STATUS,
|
|
27
|
+
exportJobEntity,
|
|
28
|
+
exportJobsTable,
|
|
29
|
+
} from "../schema/export-job";
|
|
30
|
+
|
|
31
|
+
let stack: TestStack;
|
|
32
|
+
|
|
33
|
+
const TENANT_SYSTEM = "00000000-0000-4000-8000-000000000001";
|
|
34
|
+
const ALICE_ID = "aaaaaaaa-aaaa-4aaa-8aaa-000000000001";
|
|
35
|
+
const BOB_ID = "aaaaaaaa-aaaa-4aaa-8aaa-000000000002";
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
stack = await setupTestStack({
|
|
39
|
+
features: [
|
|
40
|
+
createUserFeature(),
|
|
41
|
+
createDataRetentionFeature(),
|
|
42
|
+
createComplianceProfilesFeature(),
|
|
43
|
+
createUserDataRightsFeature(),
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await unsafeCreateEntityTable(stack.db, exportJobEntity);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await stack.cleanup();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
await stack.db.delete(exportJobsTable);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const NOW = () => getTemporal().Now.instant();
|
|
59
|
+
|
|
60
|
+
// Drizzle wraps PostgresError mit "Failed query: ..."; die original
|
|
61
|
+
// PG-Message ist im `cause`. Unique-Violation hat sqlstate 23505.
|
|
62
|
+
//
|
|
63
|
+
// Constraint-Name pinnen damit der Test nur unsere Idempotency-
|
|
64
|
+
// Constraint pinst, nicht zufaellig eine andere unique-violation
|
|
65
|
+
// (z.B. UUID-Kollision auf id-PK durch wiederholtes Test-Setup) —
|
|
66
|
+
// silent-Pass durch fremden Constraint waere falsche Bestaetigung.
|
|
67
|
+
async function expectUniqueViolation(
|
|
68
|
+
promise: Promise<unknown>,
|
|
69
|
+
expectedConstraint: string,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
let caught: unknown;
|
|
72
|
+
try {
|
|
73
|
+
await promise;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
caught = e;
|
|
76
|
+
}
|
|
77
|
+
expect(caught).toBeDefined();
|
|
78
|
+
const cause = (caught as { cause?: { code?: string; constraint_name?: string } }).cause;
|
|
79
|
+
expect(cause?.code).toBe("23505");
|
|
80
|
+
expect(cause?.constraint_name).toBe(expectedConstraint);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Re-Export aus dem Schema — Single source of truth, Rename hier
|
|
84
|
+
// braeuche kein Test-Edit.
|
|
85
|
+
const IDEMPOTENCY_CONSTRAINT = ACTIVE_JOB_CONSTRAINT;
|
|
86
|
+
|
|
87
|
+
async function insertJob(
|
|
88
|
+
userId: string,
|
|
89
|
+
status: string,
|
|
90
|
+
overrides: { bytesWritten?: number } = {},
|
|
91
|
+
): Promise<string> {
|
|
92
|
+
const id = crypto.randomUUID();
|
|
93
|
+
const values: Record<string, unknown> = {
|
|
94
|
+
id,
|
|
95
|
+
tenantId: TENANT_SYSTEM,
|
|
96
|
+
userId,
|
|
97
|
+
requestedFromTenantId: TENANT_SYSTEM,
|
|
98
|
+
status,
|
|
99
|
+
requestedAt: NOW(),
|
|
100
|
+
};
|
|
101
|
+
if (overrides.bytesWritten !== undefined) {
|
|
102
|
+
values["bytesWritten"] = overrides.bytesWritten;
|
|
103
|
+
}
|
|
104
|
+
await stack.db.insert(exportJobsTable).values(values);
|
|
105
|
+
return id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("ExportJob :: Partial-UNIQUE-Index", () => {
|
|
109
|
+
test("zwei pending-Jobs fuer denselben User → DB lehnt zweiten ab", async () => {
|
|
110
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
111
|
+
|
|
112
|
+
await expectUniqueViolation(
|
|
113
|
+
insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending),
|
|
114
|
+
IDEMPOTENCY_CONSTRAINT,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
118
|
+
expect(rows).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("pending + running fuer denselben User → DB lehnt running ab", async () => {
|
|
122
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
123
|
+
|
|
124
|
+
// Zweiter "aktiver" Status (running) ist auch gesperrt — der Index
|
|
125
|
+
// greift fuer alle Status-Werte im WHERE-Set.
|
|
126
|
+
await expectUniqueViolation(
|
|
127
|
+
insertJob(ALICE_ID, EXPORT_JOB_STATUS.Running),
|
|
128
|
+
IDEMPOTENCY_CONSTRAINT,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
132
|
+
expect(rows).toHaveLength(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("pending fuer User A + pending fuer User B → beide erlaubt (per-User-scoped)", async () => {
|
|
136
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
137
|
+
await insertJob(BOB_ID, EXPORT_JOB_STATUS.Pending);
|
|
138
|
+
|
|
139
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
140
|
+
expect(rows).toHaveLength(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("done + neuer pending fuer denselben User → erlaubt (Done ausserhalb des Index-Filters)", async () => {
|
|
144
|
+
// Erster Job done (Audit-Historie)
|
|
145
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Done);
|
|
146
|
+
// Zweiter pending-Job fuer denselben User — neue Anfrage nach
|
|
147
|
+
// erfolgreichem Download
|
|
148
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
149
|
+
|
|
150
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
151
|
+
expect(rows).toHaveLength(2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("failed + neuer pending fuer denselben User → erlaubt (Retry-Pfad)", async () => {
|
|
155
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Failed);
|
|
156
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
157
|
+
|
|
158
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
159
|
+
expect(rows).toHaveLength(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("zwei done-Jobs fuer denselben User → erlaubt (Audit-Historie)", async () => {
|
|
163
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Done);
|
|
164
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Done);
|
|
165
|
+
|
|
166
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
167
|
+
expect(rows).toHaveLength(2);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("Lifecycle: pending → running blockt weitere pending-Inserts; nach running → done wieder erlaubt", async () => {
|
|
171
|
+
// Alice pending insert
|
|
172
|
+
const aliceJobId = await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
173
|
+
// Worker pickt auf — Status flip
|
|
174
|
+
await stack.db
|
|
175
|
+
.update(exportJobsTable)
|
|
176
|
+
.set({ status: EXPORT_JOB_STATUS.Running })
|
|
177
|
+
.where(eq(exportJobsTable.id, aliceJobId));
|
|
178
|
+
|
|
179
|
+
// Zweiter pending-Insert fuer Alice → faellt weiter, weil bestehender
|
|
180
|
+
// Job in running auch im Index-Filter ist.
|
|
181
|
+
await expectUniqueViolation(
|
|
182
|
+
insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending),
|
|
183
|
+
IDEMPOTENCY_CONSTRAINT,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Aber wenn der running-Job fertig wird (done), darf User wieder pending starten
|
|
187
|
+
await stack.db
|
|
188
|
+
.update(exportJobsTable)
|
|
189
|
+
.set({ status: EXPORT_JOB_STATUS.Done })
|
|
190
|
+
.where(eq(exportJobsTable.id, aliceJobId));
|
|
191
|
+
|
|
192
|
+
await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Pending);
|
|
193
|
+
const rows = await stack.db.select().from(exportJobsTable);
|
|
194
|
+
expect(rows).toHaveLength(2);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("ExportJob :: bigInt bytesWritten DB-Roundtrip", () => {
|
|
199
|
+
test("bytesWritten >2^31 schreibt + liest identisch zurueck", async () => {
|
|
200
|
+
const TWO_GB_PLUS = 3_000_000_000; // ~2.8 GB, weit ueber integer-Cap
|
|
201
|
+
const id = await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Done, {
|
|
202
|
+
bytesWritten: TWO_GB_PLUS,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const [row] = await stack.db
|
|
206
|
+
.select({ bytesWritten: exportJobsTable["bytesWritten"] })
|
|
207
|
+
.from(exportJobsTable)
|
|
208
|
+
.where(eq(exportJobsTable["id"], id));
|
|
209
|
+
|
|
210
|
+
expect(row?.bytesWritten).toBe(TWO_GB_PLUS);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("bytesWritten 2^50 (~1 PB) schreibt + liest identisch zurueck", async () => {
|
|
214
|
+
const ONE_PB_PLUS = 2 ** 50;
|
|
215
|
+
const id = await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Done, {
|
|
216
|
+
bytesWritten: ONE_PB_PLUS,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const [row] = await stack.db
|
|
220
|
+
.select({ bytesWritten: exportJobsTable["bytesWritten"] })
|
|
221
|
+
.from(exportJobsTable)
|
|
222
|
+
.where(eq(exportJobsTable["id"], id));
|
|
223
|
+
|
|
224
|
+
expect(row?.bytesWritten).toBe(ONE_PB_PLUS);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("bytesWritten als raw-SQL geprueft → DB-Spalte ist tatsaechlich BIGINT", async () => {
|
|
228
|
+
const id = await insertJob(ALICE_ID, EXPORT_JOB_STATUS.Done, {
|
|
229
|
+
bytesWritten: 1,
|
|
230
|
+
});
|
|
231
|
+
// information_schema-Lookup pinst den DB-Typ unabhaengig vom JS-
|
|
232
|
+
// Driver-Mapping — wenn jemand `case "bigInt"` zu `integer` zurueck-
|
|
233
|
+
// refactored, faellt dieser Test um obwohl die JS-Werte sich
|
|
234
|
+
// numerisch verhalten.
|
|
235
|
+
const result = await stack.db.execute(sql`
|
|
236
|
+
SELECT data_type FROM information_schema.columns
|
|
237
|
+
WHERE table_name = 'read_export_jobs' AND column_name = 'bytes_written'
|
|
238
|
+
`);
|
|
239
|
+
// biome-ignore lint/suspicious/noExplicitAny: drizzle-execute typing
|
|
240
|
+
const rows = ((result as any).rows ?? result) as Array<{ data_type: string }>;
|
|
241
|
+
expect(rows[0]?.data_type).toBe("bigint");
|
|
242
|
+
void id;
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Drift-Guard fuer ExportJob-Schema (S2.U3+U4 Atom 1).
|
|
2
|
+
//
|
|
3
|
+
// Pinst dass:
|
|
4
|
+
// - EXPORT_JOB_STATUS Constants stabil bleiben (Worker-State-Machine
|
|
5
|
+
// + UI-Polling lesen die Werte als Magic-Strings, jede Aenderung
|
|
6
|
+
// ist ein Breaking-Change).
|
|
7
|
+
// - Export-Konstanten haben sinnvolle Werte (TTL > 0, Stale-Timeout
|
|
8
|
+
// > 0, Cleanup-Grace > 0).
|
|
9
|
+
// - exportJobEntity hat alle Felder die Worker + Handler erwarten —
|
|
10
|
+
// wenn jemand `expiresAt` umbenennt, faellt dieser Test um statt
|
|
11
|
+
// der Worker erst in Production.
|
|
12
|
+
//
|
|
13
|
+
// Schema-Snapshot, kein Behavior-Test. Behavior-Tests kommen mit Atom 2
|
|
14
|
+
// (request-export.write.ts) + Atom 3 (Worker).
|
|
15
|
+
|
|
16
|
+
import { COMPLIANCE_PROFILES } from "@cosmicdrift/kumiko-framework/compliance";
|
|
17
|
+
import { describe, expect, test } from "vitest";
|
|
18
|
+
import { EXPORT_JOB_STATUS, exportJobEntity } from "../schema/export-job";
|
|
19
|
+
|
|
20
|
+
describe("EXPORT_JOB_STATUS Drift-Guard", () => {
|
|
21
|
+
test("hat genau 4 Werte (pending/running/done/failed)", () => {
|
|
22
|
+
const values = Object.values(EXPORT_JOB_STATUS);
|
|
23
|
+
expect(values).toHaveLength(4);
|
|
24
|
+
expect(values).toContain("pending");
|
|
25
|
+
expect(values).toContain("running");
|
|
26
|
+
expect(values).toContain("done");
|
|
27
|
+
expect(values).toContain("failed");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("Status-Strings sind lowercase + nur a-z (Convention-Check, keine Sortier-Aussage)", () => {
|
|
31
|
+
// Test-Name ist bewusst praezise: wir checken Format, NICHT
|
|
32
|
+
// alphabetische Sortierung — die State-Machine kuemmert sich nicht
|
|
33
|
+
// um Sort-Order, ein Re-Order der Konstanten waere kein Bug.
|
|
34
|
+
for (const value of Object.values(EXPORT_JOB_STATUS)) {
|
|
35
|
+
expect(value).toBe(value.toLowerCase());
|
|
36
|
+
expect(value).toMatch(/^[a-z]+$/);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Export-Konfig in compliance-profiles", () => {
|
|
42
|
+
// Atom 1c: TTL-Konstanten wandern aus user-data-rights/constants.ts
|
|
43
|
+
// ins compliance-profile.userRights — pro Profile konfigurierbar +
|
|
44
|
+
// per-Tenant-Override. Tests pinnen dass die Defaults pro Profile
|
|
45
|
+
// sinnvoll sind.
|
|
46
|
+
|
|
47
|
+
for (const profileKey of [
|
|
48
|
+
"eu-dsgvo",
|
|
49
|
+
"swiss-dsg",
|
|
50
|
+
"de-hr-dsgvo-hgb",
|
|
51
|
+
"minimal-no-region",
|
|
52
|
+
] as const) {
|
|
53
|
+
test(`${profileKey}: exportDownloadTtl gesetzt + > 0`, () => {
|
|
54
|
+
const profile = COMPLIANCE_PROFILES[profileKey];
|
|
55
|
+
const ttl = profile.userRights.exportDownloadTtl;
|
|
56
|
+
expect(ttl).toBeDefined();
|
|
57
|
+
// DurationSpec hat entweder days oder hours.
|
|
58
|
+
const hasDuration = ("days" in ttl && ttl.days > 0) || ("hours" in ttl && ttl.hours > 0);
|
|
59
|
+
expect(hasDuration).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test(`${profileKey}: exportStaleTimeoutMinutes > 0`, () => {
|
|
63
|
+
expect(COMPLIANCE_PROFILES[profileKey].userRights.exportStaleTimeoutMinutes).toBeGreaterThan(
|
|
64
|
+
0,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test(`${profileKey}: exportStorageCleanupGraceHours > 0`, () => {
|
|
69
|
+
expect(
|
|
70
|
+
COMPLIANCE_PROFILES[profileKey].userRights.exportStorageCleanupGraceHours,
|
|
71
|
+
).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
test("eu-dsgvo Default-TTL ist 7 Tage (sanity-Pin)", () => {
|
|
76
|
+
expect(COMPLIANCE_PROFILES["eu-dsgvo"].userRights.exportDownloadTtl).toEqual({
|
|
77
|
+
days: 7,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("Download-TTL <= 30d in jedem Profile (DSGVO-Auskunftsfrist als sanity-cap)", () => {
|
|
82
|
+
// Auskunftsfrist ist 30d — Download laenger zu halten als die
|
|
83
|
+
// Antwort-Pflicht macht keinen Sinn.
|
|
84
|
+
for (const profileKey of [
|
|
85
|
+
"eu-dsgvo",
|
|
86
|
+
"swiss-dsg",
|
|
87
|
+
"de-hr-dsgvo-hgb",
|
|
88
|
+
"minimal-no-region",
|
|
89
|
+
] as const) {
|
|
90
|
+
const ttl = COMPLIANCE_PROFILES[profileKey].userRights.exportDownloadTtl;
|
|
91
|
+
const days = "days" in ttl ? ttl.days : Math.ceil(ttl.hours / 24);
|
|
92
|
+
expect(days).toBeLessThanOrEqual(30);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("exportJobEntity Schema-Shape", () => {
|
|
98
|
+
test("hat alle Worker-relevanten Felder", () => {
|
|
99
|
+
const fieldNames = Object.keys(exportJobEntity.fields);
|
|
100
|
+
expect(fieldNames).toEqual(
|
|
101
|
+
expect.arrayContaining([
|
|
102
|
+
"userId",
|
|
103
|
+
"requestedFromTenantId",
|
|
104
|
+
"status",
|
|
105
|
+
"requestedAt",
|
|
106
|
+
"startedAt",
|
|
107
|
+
"completedAt",
|
|
108
|
+
"downloadStorageKey",
|
|
109
|
+
"expiresAt",
|
|
110
|
+
"errorMessage",
|
|
111
|
+
"bytesWritten",
|
|
112
|
+
]),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("Tabellen-Name ist read_export_jobs (snake_case-Convention)", () => {
|
|
117
|
+
expect(exportJobEntity.table).toBe("read_export_jobs");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("status-Default ist pending (neue Job-Rows starten dort)", () => {
|
|
121
|
+
const statusField = exportJobEntity.fields["status"];
|
|
122
|
+
expect(statusField).toBeDefined();
|
|
123
|
+
if (statusField && "default" in statusField) {
|
|
124
|
+
expect(statusField.default).toBe(EXPORT_JOB_STATUS.Pending);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("status-Optionen matchen die EXPORT_JOB_STATUS-Constants", () => {
|
|
129
|
+
const statusField = exportJobEntity.fields["status"];
|
|
130
|
+
if (statusField && "options" in statusField) {
|
|
131
|
+
const options = (statusField as { options: readonly string[] }).options;
|
|
132
|
+
expect([...options].sort()).toEqual(Object.values(EXPORT_JOB_STATUS).slice().sort());
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("bytesWritten ist bigInt-Field (NICHT number) — pinst Atom-1b-Migration", () => {
|
|
137
|
+
// Drift-Guard: wenn jemand zurueck auf createNumberField refactored,
|
|
138
|
+
// faellt der DB-Roundtrip-Test um (gut), aber das dauert >1s. Hier
|
|
139
|
+
// <1s + klare Fehlermeldung was das Problem ist.
|
|
140
|
+
expect(exportJobEntity.fields.bytesWritten.type).toBe("bigInt");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("userId + requestedFromTenantId + status + requestedAt sind required", () => {
|
|
144
|
+
expect(exportJobEntity.fields["userId"]?.required).toBe(true);
|
|
145
|
+
// requestedFromTenantId ist load-bearing: Atom 3b's Worker liest es
|
|
146
|
+
// fuer Profile-Resolution. Ohne required wuerde ein Insert ohne
|
|
147
|
+
// Wert silent NULL setzen + der Worker crashed bei queryAs(systemUserOf(null), ...).
|
|
148
|
+
expect(exportJobEntity.fields["requestedFromTenantId"]?.required).toBe(true);
|
|
149
|
+
expect(exportJobEntity.fields["status"]?.required).toBe(true);
|
|
150
|
+
expect(exportJobEntity.fields["requestedAt"]?.required).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("Lifecycle-Felder sind nullable (startedAt/completedAt/expiresAt/errorMessage/bytesWritten/downloadStorageKey)", () => {
|
|
154
|
+
// Diese Felder werden vom Worker gesetzt, sind beim Insert null.
|
|
155
|
+
// required ist undefined oder false — beides bedeutet nullable.
|
|
156
|
+
expect(exportJobEntity.fields.startedAt.required).not.toBe(true);
|
|
157
|
+
expect(exportJobEntity.fields.completedAt.required).not.toBe(true);
|
|
158
|
+
expect(exportJobEntity.fields.expiresAt.required).not.toBe(true);
|
|
159
|
+
expect(exportJobEntity.fields.errorMessage.required).not.toBe(true);
|
|
160
|
+
expect(exportJobEntity.fields.bytesWritten.required).not.toBe(true);
|
|
161
|
+
expect(exportJobEntity.fields.downloadStorageKey.required).not.toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Unit-Test fuer policyToStrategy — die Mapping-Tafel zwischen
|
|
2
|
+
// retention-strategy und user-data-rights-Forget-Strategy. Load-bearing
|
|
3
|
+
// weil es entscheidet ob Daten physisch geloescht oder anonymisiert
|
|
4
|
+
// werden. Memory `feedback_no_fake_tests`: das Mapping IST die Logik,
|
|
5
|
+
// nicht ein Detail.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { policyToStrategy } from "../run-forget-cleanup";
|
|
9
|
+
|
|
10
|
+
describe("policyToStrategy", () => {
|
|
11
|
+
test("hardDelete → delete (Default-Pfad: Row physisch entfernen)", () => {
|
|
12
|
+
expect(policyToStrategy("hardDelete")).toBe("delete");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("softDelete → delete (Row markieren via softDelete-Mechanik des Frameworks)", () => {
|
|
16
|
+
expect(policyToStrategy("softDelete")).toBe("delete");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("anonymize → anonymize (Row bleibt, PII raus)", () => {
|
|
20
|
+
expect(policyToStrategy("anonymize")).toBe("anonymize");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("blockDelete → anonymize (Aufbewahrungs-Pflicht respektieren, Row bleibt physisch)", () => {
|
|
24
|
+
expect(policyToStrategy("blockDelete")).toBe("anonymize");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("null (keine Policy konfiguriert) → delete (Default = Row weg)", () => {
|
|
28
|
+
expect(policyToStrategy(null)).toBe("delete");
|
|
29
|
+
});
|
|
30
|
+
});
|