dpdp-erasure-cli 1.0.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/.env.example +55 -0
- package/Dockerfile +33 -0
- package/compliance.worker.yaml +64 -0
- package/package.json +41 -0
- package/src/constants/index.ts +1 -0
- package/src/errors/fail.ts +110 -0
- package/src/errors/index.ts +4 -0
- package/src/errors/inferer.ts +166 -0
- package/src/errors/registry.ts +122 -0
- package/src/errors/types.ts +65 -0
- package/src/errors/worker.ts +161 -0
- package/src/index.ts +328 -0
- package/src/lib/crypto/digest.ts +22 -0
- package/src/lib/crypto/encoding.ts +78 -0
- package/src/lib/crypto/index.ts +2 -0
- package/src/lib/index.ts +1 -0
- package/src/modules/bootstrap/index.ts +2 -0
- package/src/modules/bootstrap/integrity.ts +38 -0
- package/src/modules/bootstrap/preflight.ts +296 -0
- package/src/modules/cli/check-integrity.ts +48 -0
- package/src/modules/cli/dry-run.ts +90 -0
- package/src/modules/cli/graph.ts +87 -0
- package/src/modules/cli/index.ts +184 -0
- package/src/modules/cli/init.ts +115 -0
- package/src/modules/cli/inspect.ts +86 -0
- package/src/modules/cli/introspector.ts +117 -0
- package/src/modules/cli/keygen.ts +38 -0
- package/src/modules/cli/scan.ts +126 -0
- package/src/modules/cli/sign.ts +50 -0
- package/src/modules/cli/ui.ts +61 -0
- package/src/modules/cli/verify-schema.ts +31 -0
- package/src/modules/cli/verify.ts +85 -0
- package/src/modules/config/compatibility.ts +271 -0
- package/src/modules/config/index.ts +4 -0
- package/src/modules/config/reader.ts +149 -0
- package/src/modules/config/signature.ts +69 -0
- package/src/modules/config/validation.ts +658 -0
- package/src/modules/crypto/aes.ts +158 -0
- package/src/modules/crypto/envelope.ts +48 -0
- package/src/modules/crypto/hmac.ts +60 -0
- package/src/modules/crypto/index.ts +3 -0
- package/src/modules/db/drift.ts +36 -0
- package/src/modules/db/graph.ts +203 -0
- package/src/modules/db/index.ts +4 -0
- package/src/modules/db/migrations.ts +254 -0
- package/src/modules/db/sql-debug.ts +61 -0
- package/src/modules/engine/blob/index.ts +3 -0
- package/src/modules/engine/blob/s3.ts +455 -0
- package/src/modules/engine/blob/store.ts +236 -0
- package/src/modules/engine/blob/types.ts +44 -0
- package/src/modules/engine/helpers/identity.ts +47 -0
- package/src/modules/engine/helpers/index.ts +4 -0
- package/src/modules/engine/helpers/outbox.ts +118 -0
- package/src/modules/engine/helpers/runtime.ts +115 -0
- package/src/modules/engine/helpers/types.ts +61 -0
- package/src/modules/engine/index.ts +6 -0
- package/src/modules/engine/notifier/config.ts +147 -0
- package/src/modules/engine/notifier/dispatcher.ts +300 -0
- package/src/modules/engine/notifier/index.ts +3 -0
- package/src/modules/engine/notifier/payload.ts +51 -0
- package/src/modules/engine/notifier/reservation.ts +153 -0
- package/src/modules/engine/notifier/types.ts +38 -0
- package/src/modules/engine/shredder.ts +254 -0
- package/src/modules/engine/types.ts +146 -0
- package/src/modules/engine/vault/compiled-targets.ts +562 -0
- package/src/modules/engine/vault/context.ts +254 -0
- package/src/modules/engine/vault/dry-run.ts +94 -0
- package/src/modules/engine/vault/execution.ts +485 -0
- package/src/modules/engine/vault/index.ts +3 -0
- package/src/modules/engine/vault/purge.ts +82 -0
- package/src/modules/engine/vault/retention.ts +124 -0
- package/src/modules/engine/vault/satellite-mutation.ts +193 -0
- package/src/modules/engine/vault/satellite.ts +103 -0
- package/src/modules/engine/vault/shadow.ts +36 -0
- package/src/modules/engine/vault/static-plan.ts +116 -0
- package/src/modules/engine/vault/store.ts +34 -0
- package/src/modules/engine/vault/vault.ts +84 -0
- package/src/modules/introspector/classifier.ts +502 -0
- package/src/modules/introspector/dag.ts +276 -0
- package/src/modules/introspector/index.ts +7 -0
- package/src/modules/introspector/naming.ts +75 -0
- package/src/modules/introspector/report.ts +153 -0
- package/src/modules/introspector/run.ts +123 -0
- package/src/modules/introspector/s3-sampler.ts +227 -0
- package/src/modules/introspector/types.ts +131 -0
- package/src/modules/introspector/yaml.ts +101 -0
- package/src/modules/network/api/control-plane.ts +275 -0
- package/src/modules/network/api/index.ts +1 -0
- package/src/modules/network/api/validation.ts +71 -0
- package/src/modules/network/index.ts +4 -0
- package/src/modules/network/object-store/aws/client.ts +444 -0
- package/src/modules/network/object-store/aws/credentials.ts +271 -0
- package/src/modules/network/object-store/aws/index.ts +2 -0
- package/src/modules/network/object-store/aws/sigv4.ts +190 -0
- package/src/modules/network/object-store/aws/type.ts +6 -0
- package/src/modules/network/object-store/index.ts +1 -0
- package/src/modules/network/outbox/dispatcher.ts +183 -0
- package/src/modules/network/outbox/index.ts +3 -0
- package/src/modules/network/outbox/process.ts +133 -0
- package/src/modules/network/outbox/shared.ts +56 -0
- package/src/modules/network/outbox/store.ts +346 -0
- package/src/modules/network/outbox/types.ts +54 -0
- package/src/modules/network/request-signing.ts +61 -0
- package/src/modules/worker/index.ts +2 -0
- package/src/modules/worker/tasks.ts +58 -0
- package/src/modules/worker/types.ts +89 -0
- package/src/modules/worker/worker.ts +243 -0
- package/src/secrets/index.ts +4 -0
- package/src/secrets/kms/index.ts +2 -0
- package/src/secrets/kms/signature.ts +82 -0
- package/src/secrets/kms/validation.ts +64 -0
- package/src/secrets/reader.ts +42 -0
- package/src/secrets/repository/crypto.ts +89 -0
- package/src/secrets/repository/index.ts +2 -0
- package/src/secrets/repository/methods.ts +37 -0
- package/src/secrets/resolvers.ts +247 -0
- package/src/secrets/signature.ts +78 -0
- package/src/types/index.ts +1 -0
- package/src/types/types.ts +23 -0
- package/src/utils/identifiers.ts +48 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/json.ts +35 -0
- package/src/utils/logger.ts +161 -0
- package/src/validation/zod.ts +70 -0
- package/tests/adversarial.test.ts +464 -0
- package/tests/blob-s3.test.ts +216 -0
- package/tests/config.test.ts +395 -0
- package/tests/control-plane-client.test.ts +108 -0
- package/tests/crypto.test.ts +106 -0
- package/tests/errors.test.ts +69 -0
- package/tests/fetch-dispatcher.test.ts +213 -0
- package/tests/graph.test.ts +84 -0
- package/tests/helpers/index.ts +101 -0
- package/tests/index-preflight.test.ts +168 -0
- package/tests/introspector-classifier.test.ts +62 -0
- package/tests/introspector-report.test.ts +85 -0
- package/tests/introspector.test.ts +394 -0
- package/tests/kms.test.ts +124 -0
- package/tests/logger.test.ts +61 -0
- package/tests/notifier.test.ts +303 -0
- package/tests/outbox.test.ts +478 -0
- package/tests/purge-policy.test.ts +124 -0
- package/tests/retention.test.ts +103 -0
- package/tests/s3-client.test.ts +110 -0
- package/tests/satellite.test.ts +119 -0
- package/tests/schema-compatibility.test.ts +237 -0
- package/tests/schema-integrity.test.ts +64 -0
- package/tests/shredder.test.ts +163 -0
- package/tests/vault.compiled-targets.test.ts +243 -0
- package/tests/vault.replica.test.ts +59 -0
- package/tests/vault.test.ts +279 -0
- package/tests/worker.retry.test.ts +291 -0
- package/tests/worker.test.ts +200 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TEST_SECRETS,
|
|
5
|
+
createTestSql,
|
|
6
|
+
dropSchemas,
|
|
7
|
+
insertUser,
|
|
8
|
+
prepareWorkerSchemas,
|
|
9
|
+
uniqueSchema,
|
|
10
|
+
} from "./helpers";
|
|
11
|
+
import { dispatchPreErasureNotice, vaultUser, type MockMailer } from "@modules/engine";
|
|
12
|
+
import { runMigrations } from "@modules/db";
|
|
13
|
+
import type { Sql } from "@/types";
|
|
14
|
+
|
|
15
|
+
describe("Notification Handshake Engine", () => {
|
|
16
|
+
let sql: Sql;
|
|
17
|
+
const schemasToDrop: string[] = [];
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
sql = createTestSql();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await dropSchemas(sql, ...schemasToDrop);
|
|
25
|
+
await sql.end();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function prepare() {
|
|
29
|
+
const appSchema = uniqueSchema("notify_app");
|
|
30
|
+
const engineSchema = uniqueSchema("notify_engine");
|
|
31
|
+
schemasToDrop.push(appSchema, engineSchema);
|
|
32
|
+
await prepareWorkerSchemas(sql, appSchema, engineSchema, { withDependencies: true });
|
|
33
|
+
return { appSchema, engineSchema };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function seedVaultedUser(appSchema: string, engineSchema: string) {
|
|
37
|
+
const vaultAt = new Date("2020-01-01T00:00:00.000Z");
|
|
38
|
+
const userId = await insertUser(sql, appSchema, "notify.me@example.com", "Notify Me");
|
|
39
|
+
await vaultUser(sql, userId, TEST_SECRETS, {
|
|
40
|
+
appSchema,
|
|
41
|
+
engineSchema,
|
|
42
|
+
now: vaultAt,
|
|
43
|
+
defaultRetentionYears: 1,
|
|
44
|
+
noticeWindowHours: 48,
|
|
45
|
+
rootTable: "users",
|
|
46
|
+
rootIdColumn: "id",
|
|
47
|
+
rootPiiColumns: {
|
|
48
|
+
email: "HMAC",
|
|
49
|
+
full_name: "STATIC_MASK",
|
|
50
|
+
},
|
|
51
|
+
satelliteTargets: [],
|
|
52
|
+
compiledTargets: [
|
|
53
|
+
{ table: `${appSchema}.users`, pii_columns: ["email", "full_name"] },
|
|
54
|
+
{
|
|
55
|
+
table: `${appSchema}.orders`,
|
|
56
|
+
parent: `${appSchema}.users`,
|
|
57
|
+
join: `${appSchema}.users.id = ${appSchema}.orders.user_id`,
|
|
58
|
+
pii_columns: [],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
return { userId, vaultAt };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
it("decrypts PII, dispatches the notice, and records the outbox event when the notice window is open", async () => {
|
|
66
|
+
const { appSchema, engineSchema } = await prepare();
|
|
67
|
+
const { userId } = await seedVaultedUser(appSchema, engineSchema);
|
|
68
|
+
const sendAt = new Date("2020-12-30T00:00:00.000Z");
|
|
69
|
+
|
|
70
|
+
const mailer: MockMailer = {
|
|
71
|
+
sendEmail: vi.fn().mockResolvedValue({
|
|
72
|
+
provider: "unit-mailer",
|
|
73
|
+
providerMessageId: "msg-123",
|
|
74
|
+
metadata: { accepted: true },
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = await dispatchPreErasureNotice(sql, userId, TEST_SECRETS, mailer, {
|
|
79
|
+
appSchema,
|
|
80
|
+
engineSchema,
|
|
81
|
+
now: sendAt,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result.action).toBe("sent");
|
|
85
|
+
expect(mailer.sendEmail).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(mailer.sendEmail).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
to: "notify.me@example.com",
|
|
89
|
+
subject: "Notice of Permanent Data Erasure",
|
|
90
|
+
body: expect.stringContaining("Dear Notify Me"),
|
|
91
|
+
idempotencyKey: expect.stringContaining(`notice:${appSchema}:users:${userId}:`),
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const [vaultRow] = await sql`
|
|
96
|
+
SELECT notification_sent_at
|
|
97
|
+
FROM ${sql(engineSchema)}.pii_vault
|
|
98
|
+
WHERE root_schema = ${appSchema}
|
|
99
|
+
AND root_table = 'users'
|
|
100
|
+
AND root_id = ${userId.toString()}
|
|
101
|
+
`;
|
|
102
|
+
const outboxRows = await sql`
|
|
103
|
+
SELECT *
|
|
104
|
+
FROM ${sql(engineSchema)}.outbox
|
|
105
|
+
WHERE idempotency_key = ${`notice:${appSchema}:users:${userId}`}
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
expect(vaultRow?.notification_sent_at).toBeDefined();
|
|
109
|
+
expect(outboxRows).toHaveLength(1);
|
|
110
|
+
expect(outboxRows[0]?.event_type).toBe("NOTIFICATION_SENT");
|
|
111
|
+
|
|
112
|
+
const [receipt] = await sql<{
|
|
113
|
+
provider: string;
|
|
114
|
+
provider_message_id: string | null;
|
|
115
|
+
template_version: string;
|
|
116
|
+
template_hash: string;
|
|
117
|
+
}[]>`
|
|
118
|
+
SELECT provider, provider_message_id, template_version, template_hash
|
|
119
|
+
FROM ${sql(engineSchema)}.notification_receipts
|
|
120
|
+
WHERE request_id IS NULL
|
|
121
|
+
`;
|
|
122
|
+
expect(receipt).toEqual(
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
provider: "unit-mailer",
|
|
125
|
+
provider_message_id: "msg-123",
|
|
126
|
+
template_version: "dpdp-pre-erasure-v1",
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
expect(receipt?.template_hash).toMatch(/^[a-f0-9]{64}$/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns not_due and does not send email before the notice window opens", async () => {
|
|
133
|
+
const { appSchema, engineSchema } = await prepare();
|
|
134
|
+
const { userId } = await seedVaultedUser(appSchema, engineSchema);
|
|
135
|
+
const tooEarly = new Date("2020-12-20T00:00:00.000Z");
|
|
136
|
+
|
|
137
|
+
const mailer: MockMailer = {
|
|
138
|
+
sendEmail: vi.fn().mockResolvedValue(undefined),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await dispatchPreErasureNotice(sql, userId, TEST_SECRETS, mailer, {
|
|
142
|
+
appSchema,
|
|
143
|
+
engineSchema,
|
|
144
|
+
now: tooEarly,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(result.action).toBe("not_due");
|
|
148
|
+
expect(mailer.sendEmail).not.toHaveBeenCalled();
|
|
149
|
+
|
|
150
|
+
const [vaultRow] = await sql`
|
|
151
|
+
SELECT notification_sent_at
|
|
152
|
+
FROM ${sql(engineSchema)}.pii_vault
|
|
153
|
+
WHERE root_schema = ${appSchema}
|
|
154
|
+
AND root_table = 'users'
|
|
155
|
+
AND root_id = ${userId.toString()}
|
|
156
|
+
`;
|
|
157
|
+
expect(vaultRow?.notification_sent_at).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("is idempotent after the notice has already been sent", async () => {
|
|
161
|
+
const { appSchema, engineSchema } = await prepare();
|
|
162
|
+
const { userId } = await seedVaultedUser(appSchema, engineSchema);
|
|
163
|
+
const sendAt = new Date("2020-12-30T00:00:00.000Z");
|
|
164
|
+
|
|
165
|
+
const mailer: MockMailer = {
|
|
166
|
+
sendEmail: vi.fn().mockResolvedValue(undefined),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const first = await dispatchPreErasureNotice(sql, userId, TEST_SECRETS, mailer, {
|
|
170
|
+
appSchema,
|
|
171
|
+
engineSchema,
|
|
172
|
+
now: sendAt,
|
|
173
|
+
});
|
|
174
|
+
const second = await dispatchPreErasureNotice(sql, userId, TEST_SECRETS, mailer, {
|
|
175
|
+
appSchema,
|
|
176
|
+
engineSchema,
|
|
177
|
+
now: new Date("2020-12-30T01:00:00.000Z"),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(first.action).toBe("sent");
|
|
181
|
+
expect(second.action).toBe("already_sent");
|
|
182
|
+
expect(mailer.sendEmail).toHaveBeenCalledTimes(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("releases the notice lease after a mailer failure so the job can be retried", async () => {
|
|
186
|
+
const { appSchema, engineSchema } = await prepare();
|
|
187
|
+
const { userId } = await seedVaultedUser(appSchema, engineSchema);
|
|
188
|
+
const sendAt = new Date("2020-12-30T00:00:00.000Z");
|
|
189
|
+
|
|
190
|
+
const failingMailer: MockMailer = {
|
|
191
|
+
sendEmail: vi.fn().mockRejectedValue(new Error("SMTP unavailable")),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
await expect(
|
|
195
|
+
dispatchPreErasureNotice(sql, userId, TEST_SECRETS, failingMailer, {
|
|
196
|
+
appSchema,
|
|
197
|
+
engineSchema,
|
|
198
|
+
now: sendAt,
|
|
199
|
+
})
|
|
200
|
+
).rejects.toThrow(/smtp unavailable/i);
|
|
201
|
+
|
|
202
|
+
const [vaultAfterFailure] = await sql`
|
|
203
|
+
SELECT notification_sent_at, notification_lock_id, notification_lock_expires_at
|
|
204
|
+
FROM ${sql(engineSchema)}.pii_vault
|
|
205
|
+
WHERE root_schema = ${appSchema}
|
|
206
|
+
AND root_table = 'users'
|
|
207
|
+
AND root_id = ${userId.toString()}
|
|
208
|
+
`;
|
|
209
|
+
expect(vaultAfterFailure?.notification_sent_at).toBeNull();
|
|
210
|
+
expect(vaultAfterFailure?.notification_lock_id).toBeNull();
|
|
211
|
+
expect(vaultAfterFailure?.notification_lock_expires_at).toBeNull();
|
|
212
|
+
|
|
213
|
+
const goodMailer: MockMailer = {
|
|
214
|
+
sendEmail: vi.fn().mockResolvedValue(undefined),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const retry = await dispatchPreErasureNotice(sql, userId, TEST_SECRETS, goodMailer, {
|
|
218
|
+
appSchema,
|
|
219
|
+
engineSchema,
|
|
220
|
+
now: new Date("2020-12-30T00:30:00.000Z"),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(retry.action).toBe("sent");
|
|
224
|
+
expect(goodMailer.sendEmail).toHaveBeenCalledTimes(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("uses configured notice columns instead of hardcoded email/full_name keys", async () => {
|
|
228
|
+
const appSchema = uniqueSchema("notify_custom_app");
|
|
229
|
+
const engineSchema = uniqueSchema("notify_custom_engine");
|
|
230
|
+
schemasToDrop.push(appSchema, engineSchema);
|
|
231
|
+
|
|
232
|
+
await dropSchemas(sql, appSchema, engineSchema);
|
|
233
|
+
await sql`CREATE SCHEMA ${sql(appSchema)}`;
|
|
234
|
+
await sql`
|
|
235
|
+
CREATE TABLE ${sql(appSchema)}.members (
|
|
236
|
+
id TEXT PRIMARY KEY,
|
|
237
|
+
user_email TEXT NOT NULL,
|
|
238
|
+
display_name TEXT NOT NULL
|
|
239
|
+
)
|
|
240
|
+
`;
|
|
241
|
+
await sql`
|
|
242
|
+
CREATE TABLE ${sql(appSchema)}.member_orders (
|
|
243
|
+
id SERIAL PRIMARY KEY,
|
|
244
|
+
member_id TEXT NOT NULL REFERENCES ${sql(appSchema)}.members(id)
|
|
245
|
+
)
|
|
246
|
+
`;
|
|
247
|
+
await sql`
|
|
248
|
+
INSERT INTO ${sql(appSchema)}.members (id, user_email, display_name)
|
|
249
|
+
VALUES ('usr_opaque_1', 'custom@example.com', 'Custom Name')
|
|
250
|
+
`;
|
|
251
|
+
await runMigrations(sql, engineSchema);
|
|
252
|
+
|
|
253
|
+
const vaultAt = new Date("2020-01-01T00:00:00.000Z");
|
|
254
|
+
await vaultUser(sql, "usr_opaque_1", TEST_SECRETS, {
|
|
255
|
+
appSchema,
|
|
256
|
+
engineSchema,
|
|
257
|
+
now: vaultAt,
|
|
258
|
+
defaultRetentionYears: 1,
|
|
259
|
+
noticeWindowHours: 48,
|
|
260
|
+
rootTable: "members",
|
|
261
|
+
rootIdColumn: "id",
|
|
262
|
+
rootPiiColumns: {
|
|
263
|
+
user_email: "HMAC",
|
|
264
|
+
display_name: "STATIC_MASK",
|
|
265
|
+
},
|
|
266
|
+
satelliteTargets: [],
|
|
267
|
+
compiledTargets: [
|
|
268
|
+
{ table: `${appSchema}.members`, pii_columns: ["user_email", "display_name"] },
|
|
269
|
+
{
|
|
270
|
+
table: `${appSchema}.member_orders`,
|
|
271
|
+
parent: `${appSchema}.members`,
|
|
272
|
+
join: `${appSchema}.members.id = ${appSchema}.member_orders.member_id`,
|
|
273
|
+
pii_columns: [],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const mailer: MockMailer = {
|
|
279
|
+
sendEmail: vi.fn().mockResolvedValue(undefined),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = await dispatchPreErasureNotice(sql, "usr_opaque_1", TEST_SECRETS, mailer, {
|
|
283
|
+
appSchema,
|
|
284
|
+
engineSchema,
|
|
285
|
+
rootTable: "members",
|
|
286
|
+
noticeEmailColumn: "user_email",
|
|
287
|
+
noticeNameColumn: "display_name",
|
|
288
|
+
rootPiiColumns: {
|
|
289
|
+
user_email: "HMAC",
|
|
290
|
+
display_name: "STATIC_MASK",
|
|
291
|
+
},
|
|
292
|
+
now: new Date("2020-12-30T00:00:00.000Z"),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(result.action).toBe("sent");
|
|
296
|
+
expect(mailer.sendEmail).toHaveBeenCalledWith(
|
|
297
|
+
expect.objectContaining({
|
|
298
|
+
to: "custom@example.com",
|
|
299
|
+
body: expect.stringContaining("Dear Custom Name"),
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
});
|