From c427e5d4fe9c4f9f8dda196b2fc862bca3158b0b Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 12 May 2026 23:58:55 +0200 Subject: [PATCH] fix(portal-bff): set REDIS_URL + SESSION_* in auth.module.spec so ci:check passes on a clean runner (#116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary CI red on `main` after #115. Failure was masked locally because `nx test` auto-loads `apps/portal-bff/.env` — the CI runner has no such file, so `process.env.REDIS_URL` is genuinely unset there and the test sees the real failure path. Root cause: #115 made `AuthModule` import `SessionModule` so `AuthController` could inject `UserSessionIndexService`. `SessionModule` pulls in `RedisModule`, whose factory calls `assertRedisConfig()` and refuses to compile without `REDIS_URL`. The existing `auth.module.spec.ts` only set the `ENTRA_*` env vars — so as soon as the spec's `compile()` walks the new import graph, `assertRedisConfig` throws. Fix is one file: add `REDIS_URL`, `SESSION_SECRET`, `SESSION_ENCRYPTION_KEY` to the spec's `VALID` env block and dispose the `ioredis` client in `afterEach` (the spec now compiles a full SessionModule, which opens a connection at module init). Same pattern as `session.module.spec.ts`. ## Verification The reason the bug didn't surface locally was Nx's `.env` loading. To repro the CI condition locally: ``` env -u REDIS_URL -u SESSION_SECRET -u SESSION_ENCRYPTION_KEY -u DATABASE_URL \ -u ENTRA_INSTANCE_URL -u ENTRA_TENANT_ID -u ENTRA_CLIENT_ID \ -u ENTRA_CLIENT_SECRET -u ENTRA_REDIRECT_URI -u ENTRA_POST_LOGOUT_REDIRECT_URI \ pnpm exec nx test portal-bff --skip-nx-cache ``` Before this PR (on main): `auth.module.spec.ts` fails with `REDIS_URL is not set` at `assertRedisConfig`. After: 123/123 pass under that same clean env. ## Test plan - [x] `nx test portal-bff` with all BFF env vars `unset` → **123/123 pass** (the CI condition). - [x] `nx lint portal-bff` → clean. - [x] `nx build portal-bff` → clean. - [x] Prettier-clean. - [ ] CI re-run after merge → `ci:check` green. --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/116 --- apps/portal-bff/src/auth/auth.module.spec.ts | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/portal-bff/src/auth/auth.module.spec.ts b/apps/portal-bff/src/auth/auth.module.spec.ts index bcd76d0..64226c9 100644 --- a/apps/portal-bff/src/auth/auth.module.spec.ts +++ b/apps/portal-bff/src/auth/auth.module.spec.ts @@ -1,3 +1,4 @@ +import { randomBytes } from 'node:crypto'; import { ConfidentialClientApplication } from '@azure/msal-node'; import { Test } from '@nestjs/testing'; import { LoggerModule } from 'nestjs-pino'; @@ -5,6 +6,13 @@ import { AuthModule } from './auth.module'; import { ENTRA_CONFIG, type EntraConfig } from './entra-config.token'; import { MSAL_CLIENT } from './msal-client.token'; +// AuthModule imports SessionModule (so AuthController can inject +// `UserSessionIndexService`), which transitively pulls in +// RedisModule + the session-secret / session-encryption-key +// validators. The spec has to satisfy all of those env vars or the +// test fails as soon as `compile()` walks the import graph — which +// is exactly what bit the CI on PR #115 (CI starts with a clean env; +// locally `nx test` was loading apps/portal-bff/.env and masking it). const VALID = { ENTRA_INSTANCE_URL: 'https://login.microsoftonline.com/', ENTRA_TENANT_ID: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', @@ -12,6 +20,11 @@ const VALID = { ENTRA_CLIENT_SECRET: 's3cret-value-from-entra', ENTRA_REDIRECT_URI: 'http://localhost:3000/api/auth/callback', ENTRA_POST_LOGOUT_REDIRECT_URI: 'http://localhost:4200/', + // Well-formed but unreachable Redis URL — `ioredis` opens the + // socket lazily so the module compiles without any network access. + REDIS_URL: 'redis://default:test-pass@127.0.0.1:65535/0', + SESSION_SECRET: randomBytes(32).toString('base64url'), + SESSION_ENCRYPTION_KEY: randomBytes(32).toString('base64url'), }; // AuthModule's MSAL_CLIENT factory injects the Pino-backed `Logger` @@ -27,6 +40,7 @@ async function compile() { describe('AuthModule', () => { const originalEnv: Record = {}; + let ref: Awaited> | undefined; beforeEach(() => { for (const key of Object.keys(VALID) as Array) { @@ -35,7 +49,16 @@ describe('AuthModule', () => { } }); - afterEach(() => { + afterEach(async () => { + // SessionModule (transitive) opens an `ioredis` client at module + // init; explicitly disconnect so Jest doesn't hang on its + // reconnect timer between tests. + if (ref) { + const redis = ref.get<{ disconnect: () => void }>('REDIS_CLIENT'); + redis.disconnect(); + await ref.close(); + ref = undefined; + } for (const key of Object.keys(VALID) as Array) { const saved = originalEnv[key]; if (saved === undefined) { @@ -47,14 +70,14 @@ describe('AuthModule', () => { }); it('provides EntraConfig via the ENTRA_CONFIG token', async () => { - const ref = await compile(); + ref = await compile(); const config = ref.get(ENTRA_CONFIG); expect(config.clientId).toBe(VALID.ENTRA_CLIENT_ID); expect(config.authority).toBe(`${VALID.ENTRA_INSTANCE_URL}${VALID.ENTRA_TENANT_ID}`); }); it('provides a ConfidentialClientApplication via the MSAL_CLIENT token', async () => { - const ref = await compile(); + ref = await compile(); const client = ref.get(MSAL_CLIENT); expect(client).toBeInstanceOf(ConfidentialClientApplication); });