deckhearth/test/api/auth-utils.test.js
Randall Stillwell 1629afbb76 test(auth): add vitest harness + 16 auth-focused unit tests (Brief 5 of fix-auth-bypass)
Closes AGENTS.md gotcha #11 (well, the relevant half of it — "Testing:
None yet" line in §6 is now stale).

Installs vitest@^3.2.4 (single devDep, no UI / coverage / jsdom) and
adds 16 unit tests across 3 files that lock in post-Brief-1/2/4
behavior:

  test/lib/auth-secret.test.js (3 tests)
    - JWT_SECRET exports the env value
    - JWT_TOKEN_TTL is canonical 24h
    - Module throws at load when JWT_SECRET is empty

  test/lib/permission-middleware.test.js (8 tests)
    - getUserFromRequest returns null for: missing header, non-Bearer
      scheme, malformed token, wrong-secret token, expired token,
      valid-token-no-user-row
    - Returns user object for valid token + user row
    - Brief 2 regression lock: does NOT return the synthetic admin
      shape { userId: 1, email: 'admin@tcgvault.com', role: 'admin' }
      when no Authorization header is present

  test/api/auth-utils.test.js (5 tests)
    - generateToken issues 24h JWT (exp - iat === 86400)
    - Payload includes userId, email, role
    - verifyToken round-trips valid tokens
    - Returns null for malformed / wrong-secret tokens

CI: re-enabled the previously commented-out test: job in
.github/workflows/ci.yml. Blocking (no || true wrapper) — vitest is
the first runner in this repo and we want CI red on test regression.
JWT_SECRET is set via a CI-only fake; production secret is unaffected.

Rate-limit (Brief 4) coverage deferred to a future expand-auth-tests
convoy per architect's call (R11). package.json has "type": "module"
so vitest's default Vite-based transform handles .js ESM out of the
box — no transform config needed.

Convoy: fix-auth-bypass / Brief 5 (last brief)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:12:15 -05:00

52 lines
1.7 KiB
JavaScript

import { describe, expect, it, vi } from 'vitest';
import jwt from 'jsonwebtoken';
vi.mock('../../lib/database.js', () => ({
db: { query: vi.fn() },
}));
import { JWT_SECRET } from '../../lib/auth-secret.js';
import { generateToken, verifyToken } from '../../pages/api/auth-utils.js';
const ONE_DAY_IN_SECONDS = 24 * 60 * 60;
describe('pages/api/auth-utils generateToken', () => {
it('issues a token whose lifetime is 24h (exp - iat === 86400)', () => {
const token = generateToken({ id: 7, email: 'x@y.z', role: 'user' });
const decoded = jwt.decode(token);
expect(decoded.exp - decoded.iat).toBe(ONE_DAY_IN_SECONDS);
});
it('includes userId, email, and role in the payload from the user arg', () => {
const token = generateToken({ id: 7, email: 'x@y.z', role: 'admin' });
const decoded = jwt.decode(token);
expect(decoded.userId).toBe(7);
expect(decoded.email).toBe('x@y.z');
expect(decoded.role).toBe('admin');
});
});
describe('pages/api/auth-utils verifyToken', () => {
it('returns the decoded payload for a valid token', () => {
const token = generateToken({ id: 7, email: 'x@y.z', role: 'user' });
const payload = verifyToken(token);
expect(payload).not.toBeNull();
expect(payload.userId).toBe(7);
expect(payload.email).toBe('x@y.z');
expect(payload.role).toBe('user');
});
it('returns null for a malformed token', () => {
expect(verifyToken('not-a-jwt')).toBeNull();
});
it('returns null for a token signed with a different secret', () => {
const token = jwt.sign(
{ userId: 7, email: 'x@y.z', role: 'user' },
'some-other-secret',
{ expiresIn: '1h' }
);
expect(verifyToken(token)).toBeNull();
expect(JWT_SECRET).not.toBe('some-other-secret');
});
});