Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/auth/strategies/github.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { GithubStrategy } from './github.strategy';
import { Profile } from 'passport-github2';

describe('GithubStrategy.validate (#594)', () => {
const strategy = new GithubStrategy({
get: () => ({
clientId: 'id',
clientSecret: 'secret',
oauthCallbackUrl: 'http://localhost/cb',
}),
} as never);

it('maps the passport-github2 profile into the upsert input', () => {
const profile = {
id: 'gh-1',
username: 'alice',
emails: [{ value: 'alice@example.com' }],
photos: [{ value: 'https://avatars/1' }],
} as unknown as Profile;
const done = jest.fn();

strategy.validate('at', 'rt', profile, done);

expect(done).toHaveBeenCalledWith(
null,
expect.objectContaining({
githubId: 'gh-1',
username: 'alice',
email: 'alice@example.com',
avatarUrl: 'https://avatars/1',
accessToken: 'at',
refreshToken: 'rt',
}),
);
});

it('nulls email/avatar when the profile has none', () => {
const profile = { id: 'gh-2', username: 'bob' } as unknown as Profile;
const done = jest.fn();
strategy.validate('at', 'rt', profile, done);
expect(done).toHaveBeenCalledWith(
null,
expect.objectContaining({ email: null, avatarUrl: null }),
);
});
});
69 changes: 69 additions & 0 deletions src/auth/strategies/jwt.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { cookieExtractor, JwtStrategy } from './jwt.strategy';

function reqWithCookie(header?: string): Request {
return { headers: header ? { cookie: header } : {} } as unknown as Request;
}

describe('cookieExtractor (#593)', () => {
it('returns the access_token cookie value', () => {
const req = reqWithCookie('access_token=abc123; other=x');
expect(cookieExtractor(req)).toBe('abc123');
});

it('prefers req.cookies when populated', () => {
const req = {
cookies: { access_token: 'from-cookies' },
} as unknown as Request;
expect(cookieExtractor(req)).toBe('from-cookies');
});

it('decodes a URI-encoded token', () => {
const req = reqWithCookie('access_token=abc%2Fdef');
expect(cookieExtractor(req)).toBe('abc/def');
});

it('returns null for a malformed (un-decodable) token', () => {
const req = reqWithCookie('access_token=%E0%A4%A'); // invalid UTF-8 escape
expect(cookieExtractor(req)).toBeNull();
});

it('returns null when no cookie header or request', () => {
expect(cookieExtractor(reqWithCookie())).toBeNull();
expect(cookieExtractor(null as unknown as Request)).toBeNull();
});
});

describe('JwtStrategy.validate (#593)', () => {
const usersService = { findOneRaw: jest.fn() };
let strategy: JwtStrategy;

beforeEach(() => {
jest.clearAllMocks();
strategy = new JwtStrategy(
{ get: () => ({ secret: 'test' }) } as never,
usersService as never,
);
});

it('rejects a payload with no sub', async () => {
await expect(
strategy.validate({ sub: '', username: 'x' }),
).rejects.toBeInstanceOf(UnauthorizedException);
});

it('rejects a token whose user no longer exists', async () => {
usersService.findOneRaw.mockRejectedValue(new Error('not found'));
await expect(
strategy.validate({ sub: 'u1', username: 'x' }),
).rejects.toBeInstanceOf(UnauthorizedException);
});

it('returns the identity for a valid token', async () => {
usersService.findOneRaw.mockResolvedValue({ id: 'u1' });
await expect(
strategy.validate({ sub: 'u1', username: 'alice' }),
).resolves.toEqual({ userId: 'u1', username: 'alice' });
});
});
4 changes: 3 additions & 1 deletion src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export interface JwtPayload {
username: string;
}

function cookieExtractor(req: Request): string | null {
// Exported for the spec (#593) — the cookie-parsing + decodeURIComponent
// guard are security-relevant (a malformed cookie must return null, not throw).
export function cookieExtractor(req: Request): string | null {
if (!req) return null;
// If cookie-parser is installed, req.cookies will be populated.
const cookies = (req as unknown as { cookies?: Record<string, string> })
Expand Down