Files
Claude 495b91bf7f Rendre l'image constructible sans les secrets de production
La construction échouait : `src/lib/env.ts` validait le contrat d'environnement
à l'import, et `src/server/db.ts` ouvrait la connexion Prisma de même. Next.js
évalue les modules serveur pendant la construction, où ni la base ni la clé
n'existent — l'image exigeait donc les secrets de production pour être bâtie,
c'est-à-dire de les confier au constructeur. C'était à l'envers : ce sont des
valeurs d'exécution.

Les deux sont désormais résolus à la première lecture. La garantie ne bouge
pas : le premier appel utile arrive bien avant qu'une requête aboutisse, et
échoue avec le même message. Éprouvé en construisant sans aucune variable, ce
que fait exactement Docker.

Les tests décrivaient l'ancien contrat — ils restauraient l'environnement avant
de lire, ce qui ne prouvait plus rien une fois la lecture différée. Réécrits
pour lire pendant que les valeurs sont posées, avec deux cas de plus :
l'import seul n'exige rien, et la lecture d'une configuration absente échoue.

Ajout d'un `.dockerignore`, absent jusqu'ici. `.env` en tête, et ce n'est pas un
détail : il porte la clé de chiffrement et les mots de passe de la base, et
copié dans le contexte il aurait fini dans une couche de l'image, lisible par
quiconque la récupère.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-09 10:13:32 +00:00

84 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest';
/**
* Le module de configuration valide à la **première lecture**, pas à l'import :
* Next.js évalue les modules serveur pendant la construction de l'image, où ni
* la base ni la clé n'existent.
*
* Les valeurs doivent donc rester en place pendant la lecture — les restaurer
* avant de lire ne prouverait rien, sinon que le module n'a rien lu.
*/
async function withEnv<T>(
values: Record<string, string | undefined>,
read: (env: typeof import('@/lib/env').env) => T,
): Promise<T> {
const previous = { ...process.env };
process.env = { ...previous, ...values } as NodeJS.ProcessEnv;
try {
vi.resetModules();
const { env } = await import('@/lib/env');
return read(env);
} finally {
process.env = previous;
}
}
const VALID_KEY = Buffer.alloc(32, 7).toString('base64');
describe('configuration d’environnement', () => {
it('accepte une configuration complète', async () => {
const values = await withEnv(
{
DATABASE_URL: 'postgresql://user:pass@localhost:5432/planflow',
ENCRYPTION_KEY: VALID_KEY,
APP_URL: 'https://planflow.example',
},
(env) => ({ database: env.DATABASE_URL, app: env.APP_URL }),
);
expect(values.database).toContain('planflow');
expect(values.app).toBe('https://planflow.example');
});
it('n’exige rien tant qu’aucune valeur n’est lue', async () => {
// C'est ce qui rend l'image constructible sans les secrets de production :
// importer le module ne doit pas suffire à le faire échouer.
await expect(
withEnv({ DATABASE_URL: undefined, ENCRYPTION_KEY: undefined }, () => 'ok'),
).resolves.toBe('ok');
});
it('refuse une clé de chiffrement qui ne fait pas 32 octets', async () => {
await expect(
withEnv(
{
DATABASE_URL: 'postgresql://user:pass@localhost:5432/planflow',
ENCRYPTION_KEY: Buffer.alloc(16, 1).toString('base64'),
},
(env) => env.ENCRYPTION_KEY,
),
).rejects.toThrow(/ENCRYPTION_KEY/);
});
it('refuse une URL de base non PostgreSQL', async () => {
await expect(
withEnv(
{
DATABASE_URL: 'mysql://user:pass@localhost:3306/planflow',
ENCRYPTION_KEY: VALID_KEY,
},
(env) => env.DATABASE_URL,
),
).rejects.toThrow(/DATABASE_URL/);
});
it('refuse une configuration absente, à la lecture', async () => {
await expect(
withEnv(
{ DATABASE_URL: undefined, ENCRYPTION_KEY: undefined },
(env) => env.DATABASE_URL,
),
).rejects.toThrow(/DATABASE_URL/);
});
});