@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.
- package/LICENSE +34 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.d.ts +63 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.d.ts.map +1 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.js +110 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.js.map +1 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.d.ts +30 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.d.ts.map +1 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.js +61 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.js.map +1 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts +26 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts.map +1 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js +54 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js.map +1 -0
- package/dist/esm/index.d.ts +88 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +125 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/minio-self/minio-self.infra.module.d.ts +64 -0
- package/dist/esm/minio-self/minio-self.infra.module.d.ts.map +1 -0
- package/dist/esm/minio-self/minio-self.infra.module.js +105 -0
- package/dist/esm/minio-self/minio-self.infra.module.js.map +1 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts +52 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts.map +1 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js +201 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js.map +1 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts +81 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts.map +1 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.js +311 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.js.map +1 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.d.ts +70 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.d.ts.map +1 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.js +328 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.js.map +1 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts +88 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts.map +1 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js +473 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js.map +1 -0
- package/dist/esm/redis-self/redis-self.infra.module.d.ts +89 -0
- package/dist/esm/redis-self/redis-self.infra.module.d.ts.map +1 -0
- package/dist/esm/redis-self/redis-self.infra.module.js +331 -0
- package/dist/esm/redis-self/redis-self.infra.module.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +52 -0
- package/src/aws-s3/__tests__/aws-s3.infra.module.test.ts +67 -0
- package/src/aws-s3/aws-s3.infra.module.ts +120 -0
- package/src/clamav-self/__tests__/clamav-self.infra.module.test.ts +65 -0
- package/src/clamav-self/clamav-self.infra.module.ts +72 -0
- package/src/gotenberg-self/__tests__/gotenberg-self.infra.module.test.ts +65 -0
- package/src/gotenberg-self/gotenberg-self.infra.module.ts +65 -0
- package/src/index.ts +133 -0
- package/src/minio-self/__tests__/minio-self.infra.module.test.ts +70 -0
- package/src/minio-self/minio-self.infra.module.ts +116 -0
- package/src/mongodb-atlas/__tests__/mongodb-atlas.infra.module.test.ts +198 -0
- package/src/mongodb-atlas/mongodb-atlas.infra.module.ts +236 -0
- package/src/mongodb-self/__tests__/mongodb-self.infra.module.test.ts +419 -0
- package/src/mongodb-self/mongodb-self.infra.module.ts +384 -0
- package/src/postgres-self/__tests__/postgres-self.infra.module.test.ts +344 -0
- package/src/postgres-self/postgres-self.infra.module.ts +377 -0
- package/src/rabbitmq-self/__tests__/rabbitmq-self.infra.module.test.ts +499 -0
- package/src/rabbitmq-self/rabbitmq-self.infra.module.ts +531 -0
- package/src/redis-self/__tests__/redis-self.infra.module.test.ts +341 -0
- package/src/redis-self/redis-self.infra.module.ts +372 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `RedisSelfInfraProviderModule` (Slice 4.B.1 of
|
|
3
|
+
* `infra-provider-planner.md`).
|
|
4
|
+
*
|
|
5
|
+
* Goals:
|
|
6
|
+
* - Pin the `plan()` shape so a future regression that drops e.g.
|
|
7
|
+
* `REDIS_ADMIN_PASSWORD` from `secretEnvVars` fails loud.
|
|
8
|
+
* - Pin the orchestrator-pattern contract: `mintAppCredentials`
|
|
9
|
+
* accepts the password via `secretInputs` and echoes it back —
|
|
10
|
+
* proves the module is upsert-only, not generator.
|
|
11
|
+
* - Pin the username + keyPattern conventions against the existing
|
|
12
|
+
* `wildo-application-lifecycle.service.ts:441, 536` conventions so
|
|
13
|
+
* a future code change here surfaces as a diff in this test, not
|
|
14
|
+
* as a silent ACL drift.
|
|
15
|
+
* - Pin the `revokeAppCredentials` upsert-with-persist pairing.
|
|
16
|
+
* - Pin defensive validation of empty `applicationNamespace` and
|
|
17
|
+
* missing `secretInputs` keys.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
21
|
+
import {
|
|
22
|
+
AppCredentialMintingContext,
|
|
23
|
+
IRedisAclClient,
|
|
24
|
+
InfraCapability,
|
|
25
|
+
InfraPlatformAdminConsumer,
|
|
26
|
+
InfraProvider,
|
|
27
|
+
ApplicationRuntimePrincipalKind,
|
|
28
|
+
buildMinionRedisKeyPrefix,
|
|
29
|
+
} from '@wildo-ai/platform-config-lib';
|
|
30
|
+
import { RedisSelfInfraProviderModule } from '../redis-self.infra.module';
|
|
31
|
+
|
|
32
|
+
function makeAdminClientStub(): IRedisAclClient & {
|
|
33
|
+
upsertUserSpy: ReturnType<typeof vi.fn>;
|
|
34
|
+
deleteUserSpy: ReturnType<typeof vi.fn>;
|
|
35
|
+
persistAclSpy: ReturnType<typeof vi.fn>;
|
|
36
|
+
} {
|
|
37
|
+
const upsertUserSpy = vi.fn().mockResolvedValue(undefined);
|
|
38
|
+
const deleteUserSpy = vi.fn().mockResolvedValue(undefined);
|
|
39
|
+
const persistAclSpy = vi.fn().mockResolvedValue(undefined);
|
|
40
|
+
return {
|
|
41
|
+
upsertUser: upsertUserSpy,
|
|
42
|
+
deleteUser: deleteUserSpy,
|
|
43
|
+
persistAcl: persistAclSpy,
|
|
44
|
+
upsertUserSpy,
|
|
45
|
+
deleteUserSpy,
|
|
46
|
+
persistAclSpy,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeMintContext(
|
|
51
|
+
adminClient: IRedisAclClient,
|
|
52
|
+
overrides: Partial<AppCredentialMintingContext> = {},
|
|
53
|
+
): AppCredentialMintingContext {
|
|
54
|
+
return {
|
|
55
|
+
appSlug: 'wonder-todos',
|
|
56
|
+
applicationNamespace: 'wonder-todos-local',
|
|
57
|
+
wildoApplicationId: '60d21bdf1c4ae5cda1b2c3d4',
|
|
58
|
+
adminClient,
|
|
59
|
+
secretInputs: {
|
|
60
|
+
'database.redis.password': 'orchestrator-supplied-password',
|
|
61
|
+
},
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('RedisSelfInfraProviderModule.plan()', () => {
|
|
67
|
+
const contribution = RedisSelfInfraProviderModule.plan({
|
|
68
|
+
environment: 'local',
|
|
69
|
+
runtime: 'docker-compose',
|
|
70
|
+
backingServiceKey: 'redis',
|
|
71
|
+
config: {},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('declares the right provider + capability triple', () => {
|
|
75
|
+
expect(RedisSelfInfraProviderModule.provider).toBe(InfraProvider.REDIS_SELF);
|
|
76
|
+
expect(RedisSelfInfraProviderModule.supportedCapabilities).toEqual([
|
|
77
|
+
InfraCapability.CACHE_MANAGER,
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('contributes the valkey compose service + K8s manifest', () => {
|
|
82
|
+
expect(contribution.dockerComposeServices).toEqual(['valkey']);
|
|
83
|
+
expect(contribution.k8sResources).toEqual([
|
|
84
|
+
'templates/infra/k8s/internal/valkey/deployment.yaml.hbs',
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('declares platformAdmin (audience-1) for apps-manager + companion only', () => {
|
|
89
|
+
expect(contribution.platformAdmin?.envVars).toEqual({
|
|
90
|
+
REDIS_HOST: 'valkey',
|
|
91
|
+
REDIS_PORT: '6379',
|
|
92
|
+
REDIS_DATABASE: '0',
|
|
93
|
+
});
|
|
94
|
+
expect(contribution.platformAdmin?.secretEnvVars).toEqual([
|
|
95
|
+
'REDIS_ADMIN_PASSWORD',
|
|
96
|
+
]);
|
|
97
|
+
// Audience-1 only — Redis has no audience-2 platform-service-scoped
|
|
98
|
+
// use case today (D-4 of Slice 4.B locked decisions).
|
|
99
|
+
expect(contribution.platformAdmin?.consumers).toEqual([
|
|
100
|
+
InfraPlatformAdminConsumer.APPS_MANAGER,
|
|
101
|
+
InfraPlatformAdminConsumer.COMPANION,
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('declares appRuntime blob paths for the customer-app pod', () => {
|
|
106
|
+
expect(contribution.appRuntime?.perAppConfigBlobPaths).toEqual({
|
|
107
|
+
'database.redis.host': 'valkey',
|
|
108
|
+
'database.redis.port': 6379,
|
|
109
|
+
'database.redis.db': 0,
|
|
110
|
+
});
|
|
111
|
+
expect(contribution.appRuntime?.perAppSecretBlobPaths).toEqual([
|
|
112
|
+
'database.redis.username',
|
|
113
|
+
'database.redis.password',
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('RedisSelfInfraProviderModule.mintAppCredentials', () => {
|
|
119
|
+
it('upserts an ACL user with namespace-derived username and slug-derived keyPattern, then persists the ACL list', async () => {
|
|
120
|
+
// ACL key/channel namespace split (2026-05-26 fix):
|
|
121
|
+
// username → applicationNamespace ('wonder-todos-local')
|
|
122
|
+
// keyPattern → appSlug + ':*' ('wonder-todos:*')
|
|
123
|
+
// channel pattern → derived inside upsertUser from the username
|
|
124
|
+
// The keyPattern uses appSlug because the backend's effective
|
|
125
|
+
// `dataServices.redis.keyPrefix` defaults to `<appSlug>:` and any
|
|
126
|
+
// authored override must start with that prefix (validated in
|
|
127
|
+
// `RedisService.connect()`). Pre-fix, both used namespace, which
|
|
128
|
+
// produced `NOPERM` for every per-app key access.
|
|
129
|
+
const adminClient = makeAdminClientStub();
|
|
130
|
+
const result = await RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
131
|
+
makeMintContext(adminClient),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(adminClient.upsertUserSpy).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(adminClient.upsertUserSpy).toHaveBeenCalledWith({
|
|
136
|
+
username: 'wonder-todos-local',
|
|
137
|
+
password: 'orchestrator-supplied-password',
|
|
138
|
+
keyPattern: 'wonder-todos:*',
|
|
139
|
+
});
|
|
140
|
+
expect(adminClient.persistAclSpy).toHaveBeenCalledTimes(1);
|
|
141
|
+
// upsert MUST come before persist (legacy pairing parity).
|
|
142
|
+
expect(adminClient.upsertUserSpy.mock.invocationCallOrder[0])
|
|
143
|
+
.toBeLessThan(adminClient.persistAclSpy.mock.invocationCallOrder[0]);
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual({
|
|
146
|
+
secretBlobValues: {
|
|
147
|
+
'database.redis.username': 'wonder-todos-local',
|
|
148
|
+
// Echo of the input password — proves upsert-only, not generator.
|
|
149
|
+
'database.redis.password': 'orchestrator-supplied-password',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('fails loud when secretInputs is missing the password', async () => {
|
|
155
|
+
const adminClient = makeAdminClientStub();
|
|
156
|
+
await expect(
|
|
157
|
+
RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
158
|
+
makeMintContext(adminClient, { secretInputs: {} }),
|
|
159
|
+
),
|
|
160
|
+
).rejects.toThrow(/missing 'database\.redis\.password'/);
|
|
161
|
+
expect(adminClient.upsertUserSpy).not.toHaveBeenCalled();
|
|
162
|
+
expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('fails loud when applicationNamespace is empty', async () => {
|
|
166
|
+
const adminClient = makeAdminClientStub();
|
|
167
|
+
await expect(
|
|
168
|
+
RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
169
|
+
makeMintContext(adminClient, { applicationNamespace: '' }),
|
|
170
|
+
),
|
|
171
|
+
).rejects.toThrow(/applicationNamespace is empty/);
|
|
172
|
+
expect(adminClient.upsertUserSpy).not.toHaveBeenCalled();
|
|
173
|
+
expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('fails loud when appSlug is empty (the ACL key pattern derives from it)', async () => {
|
|
177
|
+
// ACL key namespace must be slug-rooted; an empty slug would mint
|
|
178
|
+
// `~:*` which is a near-global grant — guard against it explicitly
|
|
179
|
+
// instead of letting Valkey accept the malformed pattern.
|
|
180
|
+
const adminClient = makeAdminClientStub();
|
|
181
|
+
await expect(
|
|
182
|
+
RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
183
|
+
makeMintContext(adminClient, { appSlug: '' }),
|
|
184
|
+
),
|
|
185
|
+
).rejects.toThrow(/appSlug is empty/);
|
|
186
|
+
expect(adminClient.upsertUserSpy).not.toHaveBeenCalled();
|
|
187
|
+
expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('keyPattern derives from appSlug independently of applicationNamespace (key/channel split)', async () => {
|
|
191
|
+
// Confirms the key vs channel namespace split is honored: a slug
|
|
192
|
+
// that differs from the namespace stem produces the slug-rooted
|
|
193
|
+
// key pattern, NOT the namespace-rooted one. This is the contract
|
|
194
|
+
// that the backend's `<appSlug>:` keyPrefix relies on.
|
|
195
|
+
const adminClient = makeAdminClientStub();
|
|
196
|
+
await RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
197
|
+
makeMintContext(adminClient, {
|
|
198
|
+
appSlug: 'acme-suite',
|
|
199
|
+
applicationNamespace: 'acme-suite-staging',
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
expect(adminClient.upsertUserSpy).toHaveBeenCalledWith({
|
|
203
|
+
username: 'acme-suite-staging',
|
|
204
|
+
password: 'orchestrator-supplied-password',
|
|
205
|
+
keyPattern: 'acme-suite:*',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('does NOT call persistAcl when upsertUser throws (atomicity guard)', async () => {
|
|
210
|
+
const adminClient = makeAdminClientStub();
|
|
211
|
+
adminClient.upsertUserSpy.mockRejectedValue(new Error('SETUSER refused'));
|
|
212
|
+
await expect(
|
|
213
|
+
RedisSelfInfraProviderModule.mintAppCredentials!(makeMintContext(adminClient)),
|
|
214
|
+
).rejects.toThrow(/SETUSER refused/);
|
|
215
|
+
expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('RedisSelfInfraProviderModule.revokeAppCredentials', () => {
|
|
220
|
+
it('deletes the per-app ACL user and persists the list', async () => {
|
|
221
|
+
const adminClient = makeAdminClientStub();
|
|
222
|
+
await RedisSelfInfraProviderModule.revokeAppCredentials!(
|
|
223
|
+
makeMintContext(adminClient),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(adminClient.deleteUserSpy).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(adminClient.deleteUserSpy).toHaveBeenCalledWith('wonder-todos-local');
|
|
228
|
+
expect(adminClient.persistAclSpy).toHaveBeenCalledTimes(1);
|
|
229
|
+
// delete MUST come before persist.
|
|
230
|
+
expect(adminClient.deleteUserSpy.mock.invocationCallOrder[0])
|
|
231
|
+
.toBeLessThan(adminClient.persistAclSpy.mock.invocationCallOrder[0]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('fails loud when applicationNamespace is empty (does not call DELUSER on empty)', async () => {
|
|
235
|
+
const adminClient = makeAdminClientStub();
|
|
236
|
+
await expect(
|
|
237
|
+
RedisSelfInfraProviderModule.revokeAppCredentials!(
|
|
238
|
+
makeMintContext(adminClient, { applicationNamespace: '' }),
|
|
239
|
+
),
|
|
240
|
+
).rejects.toThrow(/applicationNamespace is empty/);
|
|
241
|
+
expect(adminClient.deleteUserSpy).not.toHaveBeenCalled();
|
|
242
|
+
expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('RedisSelfInfraProviderModule per-principal ACL users (Step 2 / D17)', () => {
|
|
247
|
+
const MINION = { kind: ApplicationRuntimePrincipalKind.MINION, name: 'marketingScrapper' } as const;
|
|
248
|
+
|
|
249
|
+
it('leaves the APPLICATION BACKEND username exactly where it was', async () => {
|
|
250
|
+
// Load-bearing: if the backend's ACL username moved, an existing deployment's
|
|
251
|
+
// user is orphaned on the next sync and a running backend loses Redis. Both
|
|
252
|
+
// the absent-principal case (every pre-existing call site) and the explicit
|
|
253
|
+
// backend principal must render the bare namespace.
|
|
254
|
+
for (const principal of [undefined, { kind: ApplicationRuntimePrincipalKind.APPLICATION_BACKEND }]) {
|
|
255
|
+
const adminClient = makeAdminClientStub();
|
|
256
|
+
await RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
257
|
+
makeMintContext(adminClient, principal ? { principal } : {}),
|
|
258
|
+
);
|
|
259
|
+
expect(adminClient.upsertUserSpy).toHaveBeenCalledWith(
|
|
260
|
+
expect.objectContaining({ username: 'wonder-todos-local' }),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('gives a named principal its OWN ACL user', async () => {
|
|
266
|
+
const adminClient = makeAdminClientStub();
|
|
267
|
+
await RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
268
|
+
makeMintContext(adminClient, { principal: MINION }),
|
|
269
|
+
);
|
|
270
|
+
expect(adminClient.upsertUserSpy).toHaveBeenCalledWith(
|
|
271
|
+
expect.objectContaining({ username: 'wonder-todos-local-minion-marketingScrapper' }),
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('confines a MINION to its own key namespace (D20) while the backend keeps the app-wide pattern', async () => {
|
|
276
|
+
// The ACL pattern and the minion's `REDIS_KEY_PREFIX` are ONE COUPLED PAIR —
|
|
277
|
+
// a mismatch DENIES every operation the minion performs and the error names
|
|
278
|
+
// an ACL, not the mismatch. Both sides derive from
|
|
279
|
+
// `buildMinionRedisKeyPrefix`, so this asserts the CONFINEMENT and the
|
|
280
|
+
// shared derivation together.
|
|
281
|
+
const minionClient = makeAdminClientStub();
|
|
282
|
+
await RedisSelfInfraProviderModule.mintAppCredentials!(makeMintContext(minionClient, { principal: MINION }));
|
|
283
|
+
// Asserted BOTH ways on purpose: the literal catches an accidental change to
|
|
284
|
+
// the namespace shape, and the derived form pins the INVARIANT that the ACL
|
|
285
|
+
// pattern is exactly the runtime's prefix plus `*`. The CLI writes that same
|
|
286
|
+
// prefix into the minion's `REDIS_KEY_PREFIX`, so this is the seam where the
|
|
287
|
+
// two halves of the coupled pair are checked against each other.
|
|
288
|
+
expect(minionClient.upsertUserSpy).toHaveBeenCalledWith(
|
|
289
|
+
expect.objectContaining({ keyPattern: 'wonder-todos:minions:marketingScrapper:*' }),
|
|
290
|
+
);
|
|
291
|
+
expect(minionClient.upsertUserSpy).toHaveBeenCalledWith(
|
|
292
|
+
expect.objectContaining({ keyPattern: `${buildMinionRedisKeyPrefix('wonder-todos', 'marketingScrapper')}*` }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// The minion's namespace is a strict EXTENSION of the application's, which
|
|
296
|
+
// is what keeps `RedisService`'s "authored prefix must start with
|
|
297
|
+
// `<appSlug>:`" rule true without changing it.
|
|
298
|
+
expect('wonder-todos:minions:marketingScrapper:*'.startsWith('wonder-todos:')).toBe(true);
|
|
299
|
+
|
|
300
|
+
const backendClient = makeAdminClientStub();
|
|
301
|
+
await RedisSelfInfraProviderModule.mintAppCredentials!(makeMintContext(backendClient));
|
|
302
|
+
expect(backendClient.upsertUserSpy).toHaveBeenCalledWith(
|
|
303
|
+
expect.objectContaining({ keyPattern: 'wonder-todos:*' }),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('REVOKES the principal\'s own user, never the application backend\'s', async () => {
|
|
308
|
+
// The site that makes a shared derivation mandatory: a revoke that computed
|
|
309
|
+
// the username differently would DELUSER the backend's ACL user while
|
|
310
|
+
// believing it was removing a minion's — an outage caused by a cleanup.
|
|
311
|
+
const adminClient = makeAdminClientStub();
|
|
312
|
+
await RedisSelfInfraProviderModule.revokeAppCredentials!(
|
|
313
|
+
makeMintContext(adminClient, { principal: MINION }),
|
|
314
|
+
);
|
|
315
|
+
expect(adminClient.deleteUserSpy).toHaveBeenCalledWith('wonder-todos-local-minion-marketingScrapper');
|
|
316
|
+
expect(adminClient.deleteUserSpy).not.toHaveBeenCalledWith('wonder-todos-local');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('sanitises a bucket key that is not ACL-safe', async () => {
|
|
320
|
+
// A `wildo.saas.config.ts` bucket key is a TypeScript object key and may
|
|
321
|
+
// legally contain characters an ACL username should not carry to the wire.
|
|
322
|
+
const adminClient = makeAdminClientStub();
|
|
323
|
+
await RedisSelfInfraProviderModule.mintAppCredentials!(
|
|
324
|
+
makeMintContext(adminClient, {
|
|
325
|
+
principal: { kind: ApplicationRuntimePrincipalKind.WORKER, name: 'my_odd$worker' },
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
expect(adminClient.upsertUserSpy).toHaveBeenCalledWith(
|
|
329
|
+
expect.objectContaining({ username: 'wonder-todos-local-worker-my-odd-worker' }),
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('RedisSelfInfraProviderModule audience-2 absence (D-4)', () => {
|
|
335
|
+
it('does NOT declare mintPlatformServiceCredentials', () => {
|
|
336
|
+
// Redis has no audience-2 platform-service-scoped use case today
|
|
337
|
+
// (D-4 of Slice 4.B locked decisions). When/if a real need
|
|
338
|
+
// surfaces, add the method to the module.
|
|
339
|
+
expect(RedisSelfInfraProviderModule.mintPlatformServiceCredentials).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
});
|