@wildo-ai/platform-infra-providers 1.1.2

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 (62) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/aws-s3/aws-s3.infra.module.d.ts +63 -0
  3. package/dist/esm/aws-s3/aws-s3.infra.module.d.ts.map +1 -0
  4. package/dist/esm/aws-s3/aws-s3.infra.module.js +110 -0
  5. package/dist/esm/aws-s3/aws-s3.infra.module.js.map +1 -0
  6. package/dist/esm/clamav-self/clamav-self.infra.module.d.ts +30 -0
  7. package/dist/esm/clamav-self/clamav-self.infra.module.d.ts.map +1 -0
  8. package/dist/esm/clamav-self/clamav-self.infra.module.js +61 -0
  9. package/dist/esm/clamav-self/clamav-self.infra.module.js.map +1 -0
  10. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts +26 -0
  11. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts.map +1 -0
  12. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js +54 -0
  13. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js.map +1 -0
  14. package/dist/esm/index.d.ts +88 -0
  15. package/dist/esm/index.d.ts.map +1 -0
  16. package/dist/esm/index.js +125 -0
  17. package/dist/esm/index.js.map +1 -0
  18. package/dist/esm/minio-self/minio-self.infra.module.d.ts +64 -0
  19. package/dist/esm/minio-self/minio-self.infra.module.d.ts.map +1 -0
  20. package/dist/esm/minio-self/minio-self.infra.module.js +105 -0
  21. package/dist/esm/minio-self/minio-self.infra.module.js.map +1 -0
  22. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts +52 -0
  23. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts.map +1 -0
  24. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js +201 -0
  25. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js.map +1 -0
  26. package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts +81 -0
  27. package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts.map +1 -0
  28. package/dist/esm/mongodb-self/mongodb-self.infra.module.js +311 -0
  29. package/dist/esm/mongodb-self/mongodb-self.infra.module.js.map +1 -0
  30. package/dist/esm/postgres-self/postgres-self.infra.module.d.ts +70 -0
  31. package/dist/esm/postgres-self/postgres-self.infra.module.d.ts.map +1 -0
  32. package/dist/esm/postgres-self/postgres-self.infra.module.js +328 -0
  33. package/dist/esm/postgres-self/postgres-self.infra.module.js.map +1 -0
  34. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts +88 -0
  35. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts.map +1 -0
  36. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js +473 -0
  37. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js.map +1 -0
  38. package/dist/esm/redis-self/redis-self.infra.module.d.ts +89 -0
  39. package/dist/esm/redis-self/redis-self.infra.module.d.ts.map +1 -0
  40. package/dist/esm/redis-self/redis-self.infra.module.js +331 -0
  41. package/dist/esm/redis-self/redis-self.infra.module.js.map +1 -0
  42. package/dist/tsconfig.build.tsbuildinfo +1 -0
  43. package/package.json +52 -0
  44. package/src/aws-s3/__tests__/aws-s3.infra.module.test.ts +67 -0
  45. package/src/aws-s3/aws-s3.infra.module.ts +120 -0
  46. package/src/clamav-self/__tests__/clamav-self.infra.module.test.ts +65 -0
  47. package/src/clamav-self/clamav-self.infra.module.ts +72 -0
  48. package/src/gotenberg-self/__tests__/gotenberg-self.infra.module.test.ts +65 -0
  49. package/src/gotenberg-self/gotenberg-self.infra.module.ts +65 -0
  50. package/src/index.ts +133 -0
  51. package/src/minio-self/__tests__/minio-self.infra.module.test.ts +70 -0
  52. package/src/minio-self/minio-self.infra.module.ts +116 -0
  53. package/src/mongodb-atlas/__tests__/mongodb-atlas.infra.module.test.ts +198 -0
  54. package/src/mongodb-atlas/mongodb-atlas.infra.module.ts +236 -0
  55. package/src/mongodb-self/__tests__/mongodb-self.infra.module.test.ts +419 -0
  56. package/src/mongodb-self/mongodb-self.infra.module.ts +384 -0
  57. package/src/postgres-self/__tests__/postgres-self.infra.module.test.ts +344 -0
  58. package/src/postgres-self/postgres-self.infra.module.ts +377 -0
  59. package/src/rabbitmq-self/__tests__/rabbitmq-self.infra.module.test.ts +499 -0
  60. package/src/rabbitmq-self/rabbitmq-self.infra.module.ts +531 -0
  61. package/src/redis-self/__tests__/redis-self.infra.module.test.ts +341 -0
  62. package/src/redis-self/redis-self.infra.module.ts +372 -0
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Tests for `MongodbSelfInfraProviderModule` (Slice 4.B.3 of
3
+ * `infra-provider-planner.md`).
4
+ */
5
+
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import {
8
+ AppCredentialMintingContext,
9
+ IMongoSelfAdminClient,
10
+ InfraCapability,
11
+ InfraPlatformAdminConsumer,
12
+ InfraProvider,
13
+ } from '@wildo-ai/platform-config-lib';
14
+ import { ApplicationRuntimePrincipalKind } from '@wildo-ai/platform-config-lib';
15
+ import type { Mock } from 'vitest';
16
+
17
+ // Derived from the CONTRACT rather than hand-written, so the stub cannot drift from the method it
18
+ // doubles. `ReturnType<typeof vi.fn>` erased both arity and shape, leaving every assertion on the
19
+ // recorded call unchecked.
20
+ type CollectionScopedUpsertCall = Parameters<IMongoSelfAdminClient['upsertCollectionScopedDatabaseUser']>[0];
21
+ type CollectionScopedDropCall = Parameters<IMongoSelfAdminClient['dropCollectionScopedDatabaseUser']>[0];
22
+
23
+ import { MongodbSelfInfraProviderModule, buildMinionMongodbPrivileges } from '../mongodb-self.infra.module';
24
+
25
+ function makeAdminClientStub(): IMongoSelfAdminClient & {
26
+ upsertApplicationDatabaseUserSpy: ReturnType<typeof vi.fn>;
27
+ deleteApplicationDatabaseUserSpy: ReturnType<typeof vi.fn>;
28
+ upsertCollectionScopedDatabaseUserSpy: Mock<[CollectionScopedUpsertCall], Promise<undefined>>;
29
+ dropCollectionScopedDatabaseUserSpy: Mock<[CollectionScopedDropCall], Promise<undefined>>;
30
+ } {
31
+ const upsertApplicationDatabaseUserSpy = vi.fn().mockResolvedValue(undefined);
32
+ const deleteApplicationDatabaseUserSpy = vi.fn().mockResolvedValue(undefined);
33
+ // Declares the one argument production passes, so `mock.calls[0][0]` is a real tuple element.
34
+ // A zero-arg `vi.fn()` types `calls` as `[]`, and every index into it is a TS2493.
35
+ const upsertCollectionScopedDatabaseUserSpy = vi.fn((_options: CollectionScopedUpsertCall) => Promise.resolve(undefined));
36
+ const dropCollectionScopedDatabaseUserSpy = vi.fn((_options: CollectionScopedDropCall) => Promise.resolve(undefined));
37
+ return {
38
+ upsertApplicationDatabaseUser: upsertApplicationDatabaseUserSpy,
39
+ deleteApplicationDatabaseUser: deleteApplicationDatabaseUserSpy,
40
+ upsertCollectionScopedDatabaseUser: upsertCollectionScopedDatabaseUserSpy,
41
+ dropCollectionScopedDatabaseUser: dropCollectionScopedDatabaseUserSpy,
42
+ upsertApplicationDatabaseUserSpy,
43
+ deleteApplicationDatabaseUserSpy,
44
+ upsertCollectionScopedDatabaseUserSpy,
45
+ dropCollectionScopedDatabaseUserSpy,
46
+ };
47
+ }
48
+
49
+ function makeMintContext(
50
+ adminClient: IMongoSelfAdminClient,
51
+ overrides: Partial<AppCredentialMintingContext> = {},
52
+ ): AppCredentialMintingContext {
53
+ return {
54
+ appSlug: 'wonder-todos',
55
+ applicationNamespace: 'wonder-todos-local',
56
+ wildoApplicationId: '60d21bdf1c4ae5cda1b2c3d4',
57
+ adminClient,
58
+ secretInputs: {
59
+ 'database.mongodb.password': 'orchestrator-supplied-password',
60
+ },
61
+ applicationConfigInputs: {
62
+ databaseName: 'wonder_todos',
63
+ adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin?authSource=admin',
64
+ },
65
+ ...overrides,
66
+ };
67
+ }
68
+
69
+ describe('MongodbSelfInfraProviderModule.plan()', () => {
70
+ const contribution = MongodbSelfInfraProviderModule.plan({
71
+ environment: 'local',
72
+ runtime: 'docker-compose',
73
+ backingServiceKey: 'mongodb',
74
+ config: {},
75
+ });
76
+
77
+ it('declares the right provider + capability triple', () => {
78
+ expect(MongodbSelfInfraProviderModule.provider).toBe(InfraProvider.MONGODB_SELF);
79
+ expect(MongodbSelfInfraProviderModule.supportedCapabilities).toEqual([
80
+ InfraCapability.DOCUMENT_DATABASE,
81
+ ]);
82
+ });
83
+
84
+ it('contributes the mongodb compose service + K8s manifest', () => {
85
+ expect(contribution.dockerComposeServices).toEqual(['mongodb']);
86
+ expect(contribution.k8sResources).toEqual([
87
+ 'templates/infra/k8s/internal/mongodb/deployment.yaml.hbs',
88
+ ]);
89
+ });
90
+
91
+ it('declares platformAdmin (audience-1) for apps-manager + companion', () => {
92
+ expect(contribution.platformAdmin?.secretEnvVars).toEqual(['MONGO_CONNECT_URL']);
93
+ expect(contribution.platformAdmin?.consumers).toEqual([
94
+ InfraPlatformAdminConsumer.APPS_MANAGER,
95
+ InfraPlatformAdminConsumer.COMPANION,
96
+ ]);
97
+ });
98
+
99
+ it('declares the same per-app secret blob paths as the Atlas variant', () => {
100
+ expect(contribution.appRuntime?.perAppSecretBlobPaths).toEqual([
101
+ 'database.mongodb.username',
102
+ 'database.mongodb.password',
103
+ ]);
104
+ });
105
+ });
106
+
107
+ describe('MongodbSelfInfraProviderModule.mintAppCredentials', () => {
108
+ it('upserts a self-managed Mongo user and returns the per-app SRV URL', async () => {
109
+ const adminClient = makeAdminClientStub();
110
+ const result = await MongodbSelfInfraProviderModule.mintAppCredentials!(
111
+ makeMintContext(adminClient),
112
+ );
113
+
114
+ expect(adminClient.upsertApplicationDatabaseUserSpy).toHaveBeenCalledTimes(1);
115
+ expect(adminClient.upsertApplicationDatabaseUserSpy).toHaveBeenCalledWith({
116
+ adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin?authSource=admin',
117
+ databaseName: 'wonder_todos',
118
+ username: 'wonder-todos-local_user',
119
+ password: 'orchestrator-supplied-password',
120
+ });
121
+
122
+ expect(result.secretBlobValues).toEqual({
123
+ 'database.mongodb.username': 'wonder-todos-local_user',
124
+ 'database.mongodb.password': 'orchestrator-supplied-password',
125
+ });
126
+ // Per-app URL derived by swapping admin credentials and DB path.
127
+ expect(result.runtimeArtifacts?.connectionString).toContain('wonder-todos-local_user');
128
+ expect(result.runtimeArtifacts?.connectionString).toContain('orchestrator-supplied-password');
129
+ expect(result.runtimeArtifacts?.connectionString).toContain('/wonder_todos');
130
+ });
131
+
132
+ it('fails loud when secretInputs is missing the password', async () => {
133
+ const adminClient = makeAdminClientStub();
134
+ await expect(
135
+ MongodbSelfInfraProviderModule.mintAppCredentials!(
136
+ makeMintContext(adminClient, { secretInputs: {} }),
137
+ ),
138
+ ).rejects.toThrow(/missing 'database\.mongodb\.password'/);
139
+ expect(adminClient.upsertApplicationDatabaseUserSpy).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it('fails loud when applicationNamespace is empty', async () => {
143
+ const adminClient = makeAdminClientStub();
144
+ await expect(
145
+ MongodbSelfInfraProviderModule.mintAppCredentials!(
146
+ makeMintContext(adminClient, { applicationNamespace: '' }),
147
+ ),
148
+ ).rejects.toThrow(/applicationNamespace is empty/);
149
+ });
150
+
151
+ it('fails loud when applicationConfigInputs.databaseName is missing', async () => {
152
+ const adminClient = makeAdminClientStub();
153
+ await expect(
154
+ MongodbSelfInfraProviderModule.mintAppCredentials!(
155
+ makeMintContext(adminClient, {
156
+ applicationConfigInputs: {
157
+ adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin',
158
+ },
159
+ }),
160
+ ),
161
+ ).rejects.toThrow(/missing 'databaseName'/);
162
+ });
163
+
164
+ it('fails loud when applicationConfigInputs.adminConnectUrl is missing', async () => {
165
+ const adminClient = makeAdminClientStub();
166
+ await expect(
167
+ MongodbSelfInfraProviderModule.mintAppCredentials!(
168
+ makeMintContext(adminClient, {
169
+ applicationConfigInputs: { databaseName: 'wonder_todos' },
170
+ }),
171
+ ),
172
+ ).rejects.toThrow(/missing 'adminConnectUrl'/);
173
+ });
174
+ });
175
+
176
+ describe('MongodbSelfInfraProviderModule.revokeAppCredentials', () => {
177
+ it('deletes the per-app database user via admin connection', async () => {
178
+ const adminClient = makeAdminClientStub();
179
+ await MongodbSelfInfraProviderModule.revokeAppCredentials!(
180
+ makeMintContext(adminClient),
181
+ );
182
+ expect(adminClient.deleteApplicationDatabaseUserSpy).toHaveBeenCalledTimes(1);
183
+ expect(adminClient.deleteApplicationDatabaseUserSpy).toHaveBeenCalledWith({
184
+ adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin?authSource=admin',
185
+ username: 'wonder-todos-local_user',
186
+ });
187
+ });
188
+
189
+ it('fails loud when applicationNamespace is empty', async () => {
190
+ const adminClient = makeAdminClientStub();
191
+ await expect(
192
+ MongodbSelfInfraProviderModule.revokeAppCredentials!(
193
+ makeMintContext(adminClient, { applicationNamespace: '' }),
194
+ ),
195
+ ).rejects.toThrow(/applicationNamespace is empty/);
196
+ });
197
+
198
+ it('fails loud when applicationConfigInputs.adminConnectUrl is missing', async () => {
199
+ const adminClient = makeAdminClientStub();
200
+ await expect(
201
+ MongodbSelfInfraProviderModule.revokeAppCredentials!(
202
+ makeMintContext(adminClient, {
203
+ applicationConfigInputs: { databaseName: 'wonder_todos' },
204
+ }),
205
+ ),
206
+ ).rejects.toThrow(/missing 'adminConnectUrl'/);
207
+ });
208
+ });
209
+
210
+ describe('MongodbSelfInfraProviderModule audience-2 absence (D-4)', () => {
211
+ it('does NOT declare mintPlatformServiceCredentials', () => {
212
+ expect(MongodbSelfInfraProviderModule.mintPlatformServiceCredentials).toBeUndefined();
213
+ });
214
+ });
215
+
216
+ /**
217
+ * The MongoDB privilege derivation (Step 3 / R1 / D14).
218
+ *
219
+ * These assertions ARE the security semantics — what "read" and "write" mean
220
+ * once a minion's declaration reaches the database.
221
+ */
222
+ describe('buildMinionMongodbPrivileges', () => {
223
+ const DB = 'wonder-todos';
224
+
225
+ it('scopes every privilege to ONE named collection', () => {
226
+ // Never the database resource (`collection: ''`), which is Mongo's "every
227
+ // collection here" — that would make `resourceAccess` decorative.
228
+ const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: ['todos'], writeCollections: [] });
229
+ expect(privileges).toEqual([{ resource: { db: DB, collection: 'todos' }, actions: ['find'] }]);
230
+ expect(privileges.every((p) => p.resource.collection.length > 0)).toBe(true);
231
+ });
232
+
233
+ it('grants find for read and NOT find for write', () => {
234
+ // The platform derivation refuses to widen write into read; re-adding `find`
235
+ // here would defeat that decision at the last hop.
236
+ const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: [], writeCollections: ['todos'] });
237
+ expect(privileges[0]!.actions).toEqual(['insert', 'update', 'remove']);
238
+ expect(privileges[0]!.actions).not.toContain('find');
239
+ });
240
+
241
+ it('grants no enumeration action', () => {
242
+ // A minion granted read on one resource must not be able to discover what
243
+ // else exists.
244
+ const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: ['todos'], writeCollections: ['todos'] });
245
+ for (const action of privileges.flatMap((p) => p.actions)) {
246
+ expect(['listCollections', 'listIndexes', 'collStats', 'dbStats', 'listDatabases']).not.toContain(action);
247
+ }
248
+ });
249
+
250
+ it('NEVER emits dbAdmin or readWrite (D14)', () => {
251
+ // The two roles the application backend holds on the whole database.
252
+ // `dbAdmin` is banned outright; `readWrite` is database-WIDE, so granting it
253
+ // would reach every collection regardless of the declaration.
254
+ const serialized = JSON.stringify(buildMinionMongodbPrivileges({
255
+ databaseName: DB, readCollections: ['todos'], writeCollections: ['todos'],
256
+ }));
257
+ expect(serialized).not.toMatch(/dbAdmin|readWrite/);
258
+ });
259
+
260
+ it('keeps read and write as SEPARATE entries for a collection granted both', () => {
261
+ // Mongo unions them identically, but the emitted role then reads back as the
262
+ // declaration that produced it — so an operator inspecting `db.getRole()`
263
+ // can check the grant against the config without re-deriving it.
264
+ const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: ['todos'], writeCollections: ['todos'] });
265
+ expect(privileges).toHaveLength(2);
266
+ expect(privileges.map((p) => p.actions)).toEqual([['find'], ['insert', 'update', 'remove']]);
267
+ });
268
+
269
+ it('produces NO privileges for an empty grant set', () => {
270
+ // Declared-but-empty is a legitimate statement (a principal with no access).
271
+ // It must not degrade into "no privileges specified, therefore everything".
272
+ expect(buildMinionMongodbPrivileges({ databaseName: DB, readCollections: [], writeCollections: [] })).toEqual([]);
273
+ });
274
+ });
275
+
276
+ describe('MongodbSelfInfraProviderModule per-principal users (Step 3 / R1 / D14)', () => {
277
+ const MINION = { kind: ApplicationRuntimePrincipalKind.MINION, name: 'marketingScrapper' } as const;
278
+ const baseContext = {
279
+ appSlug: 'wonder-todos',
280
+ applicationNamespace: 'wonder_todos_local',
281
+ wildoApplicationId: '6a5f02014b25d65f9bfd70b2',
282
+ secretInputs: { 'database.mongodb.password': 'orchestrator-pw' },
283
+ applicationConfigInputs: { databaseName: 'wonder-todos', adminConnectUrl: 'mongodb://admin:pw@localhost:27017/admin' },
284
+ };
285
+
286
+ function makeAdmin() {
287
+ return {
288
+ upsertApplicationDatabaseUser: vi.fn(async () => {}),
289
+ upsertCollectionScopedDatabaseUser: vi.fn(async (_options: unknown) => {}),
290
+ deleteApplicationDatabaseUser: vi.fn(async () => {}),
291
+ };
292
+ }
293
+
294
+ it('takes the COLLECTION-SCOPED path for a minion, never the database-wide one', async () => {
295
+ const admin = makeAdmin();
296
+ await MongodbSelfInfraProviderModule.mintAppCredentials!({
297
+ ...baseContext, adminClient: admin, principal: MINION,
298
+ resourceGrants: { read: ['todos'], write: ['todos'] },
299
+ } as never);
300
+
301
+ expect(admin.upsertCollectionScopedDatabaseUser).toHaveBeenCalledTimes(1);
302
+ // The wide path grants `readWrite` + `dbAdmin` on the whole database — it
303
+ // must not be reachable for a named principal at all.
304
+ expect(admin.upsertApplicationDatabaseUser).not.toHaveBeenCalled();
305
+
306
+ const call = admin.upsertCollectionScopedDatabaseUser.mock.calls[0]![0] as never as {
307
+ username: string; roleName: string;
308
+ privileges: Array<{ resource: { collection: string }; actions: string[] }>;
309
+ };
310
+ expect(call.username).toBe('wonder_todos_local_minion_marketingScrapper_user');
311
+ expect(call.roleName).toBe('wonder_todos_local_minion_marketingScrapper_user_role');
312
+ expect(call.privileges).toEqual([
313
+ { resource: { db: 'wonder-todos', collection: 'todos' }, actions: ['find'] },
314
+ { resource: { db: 'wonder-todos', collection: 'todos' }, actions: ['insert', 'update', 'remove'] },
315
+ ]);
316
+ });
317
+
318
+ it('leaves the APPLICATION BACKEND on the database-wide path, username unmoved', async () => {
319
+ // Moving it would orphan an existing deployment's user, and the backend
320
+ // legitimately holds the wide grant today.
321
+ const admin = makeAdmin();
322
+ await MongodbSelfInfraProviderModule.mintAppCredentials!({ ...baseContext, adminClient: admin } as never);
323
+ expect(admin.upsertApplicationDatabaseUser).toHaveBeenCalledWith(
324
+ expect.objectContaining({ username: 'wonder_todos_local_user' }),
325
+ );
326
+ expect(admin.upsertCollectionScopedDatabaseUser).not.toHaveBeenCalled();
327
+ });
328
+
329
+ it('REFUSES a named principal with no grants rather than falling back to the wide user', async () => {
330
+ // The failure mode this guards: falling back would hand a minion
331
+ // `readWrite` + `dbAdmin` on the whole database through a code path that
332
+ // looks like it is doing the narrow thing. Whether to mint nothing at all
333
+ // is the ORCHESTRATOR's decision (D11/R3), not a default hidden here.
334
+ const admin = makeAdmin();
335
+ await expect(MongodbSelfInfraProviderModule.mintAppCredentials!({
336
+ ...baseContext, adminClient: admin, principal: MINION,
337
+ } as never)).rejects.toThrow(/no resourceGrants/);
338
+ expect(admin.upsertApplicationDatabaseUser).not.toHaveBeenCalled();
339
+ expect(admin.upsertCollectionScopedDatabaseUser).not.toHaveBeenCalled();
340
+ });
341
+
342
+ it('emits a role with NO privileges for a declared-but-empty grant set', async () => {
343
+ // Legitimate: a principal that declared access and asked for nothing. It
344
+ // must produce a role granting nothing, never "unspecified ⇒ leave it wide".
345
+ const admin = makeAdmin();
346
+ await MongodbSelfInfraProviderModule.mintAppCredentials!({
347
+ ...baseContext, adminClient: admin, principal: MINION, resourceGrants: { read: [], write: [] },
348
+ } as never);
349
+ expect((admin.upsertCollectionScopedDatabaseUser.mock.calls[0]![0] as never as { privileges: unknown[] }).privileges)
350
+ .toEqual([]);
351
+ });
352
+ });
353
+
354
+
355
+ /**
356
+ * Revocation of a NAMED principal (Step 3 / R1).
357
+ *
358
+ * These exist because the defect they pin was SHIPPED: `revokeAppCredentials`
359
+ * derived the username with `buildMongodbUsername(applicationNamespace)`, which
360
+ * returns the APPLICATION BACKEND's name for every input. Retiring one minion
361
+ * therefore left that minion's credential live and dropped the backend's
362
+ * database user — while the mint path a few lines above carried a comment
363
+ * warning about exactly this. The fix landed at one site of the invariant; these
364
+ * pins are the other site.
365
+ */
366
+ describe('MongodbSelfInfraProviderModule.revokeAppCredentials — named principal', () => {
367
+ const minionPrincipal = {
368
+ kind: ApplicationRuntimePrincipalKind.MINION,
369
+ name: 'marketingScrapper',
370
+ } as AppCredentialMintingContext['principal'];
371
+
372
+ it('never touches the APPLICATION BACKEND user when retiring a minion', async () => {
373
+ const adminClient = makeAdminClientStub();
374
+ await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
375
+ principal: minionPrincipal,
376
+ }));
377
+ // The user-only door is what dropped the backend's user. Asserting it was
378
+ // not called is the pin that reddens on a regression to the old derivation.
379
+ expect(adminClient.deleteApplicationDatabaseUserSpy).not.toHaveBeenCalled();
380
+ expect(adminClient.dropCollectionScopedDatabaseUserSpy).toHaveBeenCalledTimes(1);
381
+ });
382
+
383
+ it('drops the custom ROLE as well as the user', async () => {
384
+ // A role left behind is silently adopted by the next mint of the same name,
385
+ // carrying privileges nobody re-declared.
386
+ const adminClient = makeAdminClientStub();
387
+ await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
388
+ principal: minionPrincipal,
389
+ }));
390
+ const call = adminClient.dropCollectionScopedDatabaseUserSpy.mock.calls[0]![0];
391
+ expect(call.roleName).toBe(`${call.username}_role`);
392
+ expect(call.databaseName).toBe('wonder_todos');
393
+ });
394
+
395
+ it('derives the SAME username on mint and on revoke', async () => {
396
+ // Compared to each other rather than to a literal, so the pin survives a
397
+ // deliberate rename and fails only when the two sides DISAGREE — which is
398
+ // the failure that actually occurred.
399
+ const adminClient = makeAdminClientStub();
400
+ await MongodbSelfInfraProviderModule.mintAppCredentials!(makeMintContext(adminClient, {
401
+ principal: minionPrincipal,
402
+ resourceGrants: { read: ['todos'] as never, write: [] as never },
403
+ }));
404
+ await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
405
+ principal: minionPrincipal,
406
+ }));
407
+ expect(adminClient.dropCollectionScopedDatabaseUserSpy.mock.calls[0]![0].username)
408
+ .toBe(adminClient.upsertCollectionScopedDatabaseUserSpy.mock.calls[0]![0].username);
409
+ });
410
+
411
+ it('still routes the APPLICATION BACKEND through the user-only door', async () => {
412
+ const adminClient = makeAdminClientStub();
413
+ await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
414
+ principal: { kind: ApplicationRuntimePrincipalKind.APPLICATION_BACKEND } as AppCredentialMintingContext['principal'],
415
+ }));
416
+ expect(adminClient.dropCollectionScopedDatabaseUserSpy).not.toHaveBeenCalled();
417
+ expect(adminClient.deleteApplicationDatabaseUserSpy).toHaveBeenCalledTimes(1);
418
+ });
419
+ });