fix(portal-bff): set REDIS_URL + SESSION_* in auth.module.spec so ci:check passes on a clean runner (#116)
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m23s
CI / check (push) Successful in 1m33s
CI / a11y (push) Successful in 45s
CI / perf (push) Successful in 2m58s

## 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 <julien.gautier@apf.asso.fr>
Reviewed-on: #116
This commit was merged in pull request #116.
This commit is contained in:
2026-05-12 23:58:55 +02:00
parent c3de2340e7
commit c427e5d4fe
+26 -3
View File
@@ -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<string, string | undefined> = {};
let ref: Awaited<ReturnType<typeof compile>> | undefined;
beforeEach(() => {
for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
@@ -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<keyof typeof VALID>) {
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<EntraConfig>(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<ConfidentialClientApplication>(MSAL_CLIENT);
expect(client).toBeInstanceOf(ConfidentialClientApplication);
});