diff --git a/server/tests/e2e/storage-admin.e2e.test.ts b/server/tests/e2e/storage-admin.e2e.test.ts index b67c2dee1..bbb1791bc 100644 --- a/server/tests/e2e/storage-admin.e2e.test.ts +++ b/server/tests/e2e/storage-admin.e2e.test.ts @@ -109,6 +109,9 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi // class-level (JwtAuthGuard + AdminGuard on the controller), not re-added // per handler where a future handler could forget them. expect((await request(server).put('/api/admin/storage').send({ backends: [], categories: {} })).status).toBe(401); + expect( + (await request(server).post('/api/admin/storage/test').send({ backend: { name: 'x', type: 'local', options: { root: '/tmp' } } })).status, + ).toBe(401); }); it('STORE2E-002 403 for a non-admin', async () => { diff --git a/server/tests/unit/nest/storage/storage-admin.service.test.ts b/server/tests/unit/nest/storage/storage-admin.service.test.ts index 5e07a5d5a..275cc245f 100644 --- a/server/tests/unit/nest/storage/storage-admin.service.test.ts +++ b/server/tests/unit/nest/storage/storage-admin.service.test.ts @@ -320,4 +320,36 @@ describe('StorageAdminService.testBackend', () => { }), ).rejects.toThrow("re-enter the secret 'secretAccessKey' for 'ghost'"); }); + + it('STORADM-025 a mirror with a stored s3 replica decrypts the enc:v1: secret for the probe', async () => { + const goodRoot = makeTmpDir(); + const { service, uploadsRoot } = makeService(); + service.applyConfig(configWith(uploadsRoot, { + backends: [ + { name: 'good-local', type: 'local', options: { root: goodRoot } }, + { + name: 'off-box', + type: 's3', + options: { ...S3_OPTIONS, endpoint: 'http://127.0.0.1:1', retries: 0, timeoutMs: 200 }, + }, + ], + categories: {}, + })); + // applyConfig encrypted the secret at rest — the snapshot the mirror + // expansion reads holds ciphertext, so this probe exercises the full + // snapshot → decryptBackendSecrets → ephemeral S3Driver chain. + expect(readRow(BACKENDS_KEY)).toContain('enc:v1:'); + const result = await service.testBackend({ + name: 'cand-mirror', + type: 'mirror', + options: { primary: 'good-local', replicas: ['off-box'] }, + }); + expect(result.targets).toEqual([ + { name: 'good-local', ok: true }, + expect.objectContaining({ name: 'off-box', ok: false }), + ]); + // Decrypt succeeded: the replica failed on the network. A decrypt failure + // would reject the whole call (decryptBackendSecrets throws), not mark a target. + expect(result.targets[1]!.error).not.toContain('could not decrypt'); + }); }); diff --git a/server/tests/unit/nest/storage/storage-registry.service.test.ts b/server/tests/unit/nest/storage/storage-registry.service.test.ts index dcc5048f7..a414d245a 100644 --- a/server/tests/unit/nest/storage/storage-registry.service.test.ts +++ b/server/tests/unit/nest/storage/storage-registry.service.test.ts @@ -589,6 +589,33 @@ describe('seed-once storage-config.json import', () => { expect(readRow('storage.backends')).toBe(firstRow); expect(second.resolve('backups').backendName).toBe('nas'); }); + + it('SEED-010 an all-encrypted seed imports without ENCRYPTION_KEY (the gate is plaintext-only)', () => { + const cipher = encrypt_api_key('sk-seed'); + writeSeed( + JSON.stringify({ + backends: [ + { + name: 'off-box', + type: 's3', + options: { + endpoint: 'http://127.0.0.1:9000', + bucket: 'trek', + accessKeyId: 'ak', + secretAccessKey: cipher, + }, + }, + ], + categories: { backups: 'off-box' }, + }), + ); + const registry = makeUnseededRegistry({ encryptionKeySet: false }); + registry.onModuleInit(); + // enc:v1: values are not "plaintext secrets" — the key-presence gate must + // not fire, and the ciphertext must persist byte-for-byte (idempotent encrypt). + expect(registry.resolve('backups').driver).toBeInstanceOf(S3Driver); + expect(readRow('storage.backends')).toContain(cipher); + }); }); // ── snapshot() — effective world with provenance ──────────────────────────────