Skip to content

Commit bc539a0

Browse files
authored
Merge pull request #228 from kilodesodiq-arch/issue-227-token-verification-regression-tests
Add regression tests for token verification and audit logging
2 parents fbce5e1 + b1ecc77 commit bc539a0

4 files changed

Lines changed: 614 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { UnauthorizedException, type ExecutionContext } from '@nestjs/common';
3+
import { Reflector } from '@nestjs/core';
4+
import { JwtGuard } from './jwt.guard';
5+
import { VerificationService } from '../services/verification.service';
6+
7+
describe('JwtGuard', () => {
8+
let guard: JwtGuard;
9+
let verificationService: jest.Mocked<VerificationService>;
10+
let reflector: jest.Mocked<Reflector>;
11+
12+
const createMockContext = (headers: Record<string, string>) =>
13+
({
14+
switchToHttp: () => ({
15+
getRequest: () => ({
16+
headers,
17+
tokenPayload: undefined,
18+
tokenExpiresAt: undefined,
19+
}),
20+
}),
21+
getHandler: () => jest.fn(),
22+
getClass: () => jest.fn(),
23+
}) as unknown as ExecutionContext;
24+
25+
beforeEach(async () => {
26+
verificationService = {
27+
extractTokenFromHeader: jest.fn(),
28+
validateJwtToken: jest.fn(),
29+
} as unknown as jest.Mocked<VerificationService>;
30+
31+
reflector = {
32+
getAllAndOverride: jest.fn(),
33+
} as unknown as jest.Mocked<Reflector>;
34+
35+
const module: TestingModule = await Test.createTestingModule({
36+
providers: [
37+
JwtGuard,
38+
{ provide: VerificationService, useValue: verificationService },
39+
{ provide: Reflector, useValue: reflector },
40+
],
41+
}).compile();
42+
43+
guard = module.get<JwtGuard>(JwtGuard);
44+
});
45+
46+
it('allows activation with a valid token', async () => {
47+
const context = createMockContext({ authorization: 'Bearer valid.jwt' });
48+
verificationService.extractTokenFromHeader.mockReturnValue('valid.jwt');
49+
verificationService.validateJwtToken.mockResolvedValue({
50+
isValid: true,
51+
payload: { sub: 'user-123' },
52+
});
53+
54+
const result = await guard.canActivate(context);
55+
56+
expect(result).toBe(true);
57+
});
58+
59+
it('throws when authorization header is missing', async () => {
60+
const context = createMockContext({});
61+
62+
await expect(guard.canActivate(context)).rejects.toThrow(
63+
UnauthorizedException,
64+
);
65+
});
66+
67+
it('throws when authorization header format is invalid', async () => {
68+
const context = createMockContext({ authorization: 'Basic token' });
69+
verificationService.extractTokenFromHeader.mockReturnValue(null);
70+
71+
await expect(guard.canActivate(context)).rejects.toThrow(
72+
UnauthorizedException,
73+
);
74+
});
75+
76+
it('throws when token validation fails', async () => {
77+
const context = createMockContext({ authorization: 'Bearer invalid.jwt' });
78+
verificationService.extractTokenFromHeader.mockReturnValue('invalid.jwt');
79+
verificationService.validateJwtToken.mockResolvedValue({
80+
isValid: false,
81+
error: 'jwt expired',
82+
});
83+
84+
await expect(guard.canActivate(context)).rejects.toThrow(
85+
UnauthorizedException,
86+
);
87+
});
88+
89+
it('attaches token payload to request on success', async () => {
90+
const request: any = { headers: { authorization: 'Bearer valid.jwt' } };
91+
const context = {
92+
switchToHttp: () => ({
93+
getRequest: () => request,
94+
}),
95+
getHandler: () => jest.fn(),
96+
getClass: () => jest.fn(),
97+
} as unknown as ExecutionContext;
98+
99+
verificationService.extractTokenFromHeader.mockReturnValue('valid.jwt');
100+
verificationService.validateJwtToken.mockResolvedValue({
101+
isValid: true,
102+
payload: { sub: 'user-123' },
103+
expiresAt: new Date(),
104+
});
105+
106+
await guard.canActivate(context);
107+
108+
expect(request.tokenPayload).toEqual({ sub: 'user-123' });
109+
expect(request.tokenExpiresAt).toBeInstanceOf(Date);
110+
});
111+
112+
it('passes options from reflector to validation', async () => {
113+
const context = createMockContext({ authorization: 'Bearer my.token' });
114+
verificationService.extractTokenFromHeader.mockReturnValue('my.token');
115+
verificationService.validateJwtToken.mockResolvedValue({
116+
isValid: true,
117+
payload: { sub: 'user-123' },
118+
});
119+
reflector.getAllAndOverride.mockReturnValue({ audience: 'myapp' });
120+
121+
await guard.canActivate(context);
122+
123+
expect(verificationService.validateJwtToken).toHaveBeenCalledWith(
124+
'my.token',
125+
{ audience: 'myapp' },
126+
);
127+
});
128+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { ExecutionContext, CallHandler } from '@nestjs/common';
2+
import { of } from 'rxjs';
3+
import { TokenHeaderInterceptor } from './token-header.interceptor';
4+
5+
describe('TokenHeaderInterceptor', () => {
6+
let interceptor: TokenHeaderInterceptor;
7+
8+
const createMockContext = (requestOverrides: Record<string, any> = {}) => {
9+
const response = { setHeader: jest.fn() };
10+
const request = {
11+
tokenExpiresAt: undefined,
12+
tokenPayload: undefined,
13+
walletPayload: undefined,
14+
...requestOverrides,
15+
};
16+
17+
return {
18+
switchToHttp: () => ({
19+
getRequest: () => request,
20+
getResponse: () => response,
21+
}),
22+
getHandler: () => jest.fn(),
23+
getClass: () => jest.fn(),
24+
} as unknown as ExecutionContext;
25+
};
26+
27+
const mockCallHandler: CallHandler = {
28+
handle: () => of({ success: true }),
29+
};
30+
31+
beforeEach(() => {
32+
interceptor = new TokenHeaderInterceptor();
33+
});
34+
35+
it('sets X-Token-Expires-At header when tokenExpiresAt is present', (done) => {
36+
const expiresAt = new Date('2026-12-31T23:59:59Z');
37+
const context = createMockContext({ tokenExpiresAt: expiresAt });
38+
const response = context.switchToHttp().getResponse();
39+
40+
interceptor.intercept(context, mockCallHandler).subscribe(() => {
41+
expect(response.setHeader).toHaveBeenCalledWith(
42+
'X-Token-Expires-At',
43+
expiresAt.toISOString(),
44+
);
45+
done();
46+
});
47+
});
48+
49+
it('sets X-Token-Subject header when tokenPayload.sub exists', (done) => {
50+
const context = createMockContext({
51+
tokenPayload: { sub: 'user-789' },
52+
});
53+
const response = context.switchToHttp().getResponse();
54+
55+
interceptor.intercept(context, mockCallHandler).subscribe(() => {
56+
expect(response.setHeader).toHaveBeenCalledWith(
57+
'X-Token-Subject',
58+
'user-789',
59+
);
60+
done();
61+
});
62+
});
63+
64+
it('sets X-Wallet-Address header when walletPayload.address exists', (done) => {
65+
const context = createMockContext({
66+
walletPayload: { address: '0x123' },
67+
});
68+
const response = context.switchToHttp().getResponse();
69+
70+
interceptor.intercept(context, mockCallHandler).subscribe(() => {
71+
expect(response.setHeader).toHaveBeenCalledWith(
72+
'X-Wallet-Address',
73+
'0x123',
74+
);
75+
done();
76+
});
77+
});
78+
79+
it('does not set any headers when no token data is present', (done) => {
80+
const context = createMockContext();
81+
const response = context.switchToHttp().getResponse();
82+
83+
interceptor.intercept(context, mockCallHandler).subscribe(() => {
84+
expect(response.setHeader).not.toHaveBeenCalled();
85+
done();
86+
});
87+
});
88+
89+
it('passes through the response data unchanged', (done) => {
90+
const context = createMockContext();
91+
92+
interceptor.intercept(context, mockCallHandler).subscribe((data) => {
93+
expect(data).toEqual({ success: true });
94+
done();
95+
});
96+
});
97+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import type { ExecutionContext, CallHandler } from '@nestjs/common';
3+
import { of } from 'rxjs';
4+
import { TokenLoggingInterceptor } from './token-logging.interceptor';
5+
import { VerificationService } from '../services/verification.service';
6+
7+
describe('TokenLoggingInterceptor', () => {
8+
let interceptor: TokenLoggingInterceptor;
9+
let verificationService: jest.Mocked<VerificationService>;
10+
11+
const mockRequest = (overrides: Record<string, any> = {}) => ({
12+
headers: { authorization: 'Bearer test.jwt.token' },
13+
method: 'GET',
14+
url: '/api/test',
15+
tokenPayload: undefined,
16+
walletPayload: undefined,
17+
...overrides,
18+
});
19+
20+
const mockResponse = () => ({});
21+
22+
const createMockContext = (request: Record<string, any>) =>
23+
({
24+
switchToHttp: () => ({
25+
getRequest: () => request,
26+
getResponse: mockResponse,
27+
}),
28+
getHandler: () => jest.fn(),
29+
getClass: () => jest.fn(),
30+
}) as unknown as ExecutionContext;
31+
32+
const mockCallHandler: CallHandler = {
33+
handle: () => of({ success: true }),
34+
};
35+
36+
beforeEach(async () => {
37+
verificationService = {
38+
extractTokenFromHeader: jest.fn(),
39+
} as unknown as jest.Mocked<VerificationService>;
40+
41+
const module: TestingModule = await Test.createTestingModule({
42+
providers: [
43+
TokenLoggingInterceptor,
44+
{ provide: VerificationService, useValue: verificationService },
45+
],
46+
}).compile();
47+
48+
interceptor = module.get<TokenLoggingInterceptor>(TokenLoggingInterceptor);
49+
});
50+
51+
it('logs token usage when auth header is present', (done) => {
52+
const request = mockRequest();
53+
verificationService.extractTokenFromHeader.mockReturnValue(
54+
'test.jwt.token',
55+
);
56+
57+
interceptor
58+
.intercept(createMockContext(request), mockCallHandler)
59+
.subscribe(() => {
60+
expect(verificationService.extractTokenFromHeader).toHaveBeenCalledWith(
61+
'Bearer test.jwt.token',
62+
);
63+
done();
64+
});
65+
});
66+
67+
it('does not log when auth header is missing', (done) => {
68+
const request = mockRequest({ headers: {} });
69+
verificationService.extractTokenFromHeader.mockReturnValue(null);
70+
71+
interceptor
72+
.intercept(createMockContext(request), mockCallHandler)
73+
.subscribe(() => {
74+
expect(
75+
verificationService.extractTokenFromHeader,
76+
).not.toHaveBeenCalled();
77+
done();
78+
});
79+
});
80+
81+
it('logs successful JWT verification on response', (done) => {
82+
const request = mockRequest({
83+
tokenPayload: { sub: 'user-456' },
84+
});
85+
verificationService.extractTokenFromHeader.mockReturnValue(
86+
'test.jwt.token',
87+
);
88+
89+
interceptor
90+
.intercept(createMockContext(request), mockCallHandler)
91+
.subscribe(() => {
92+
done();
93+
});
94+
});
95+
96+
it('logs successful wallet verification on response', (done) => {
97+
const request = mockRequest({
98+
walletPayload: { address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' },
99+
});
100+
101+
interceptor
102+
.intercept(createMockContext(request), mockCallHandler)
103+
.subscribe(() => {
104+
done();
105+
});
106+
});
107+
108+
it('handles request without token or wallet payload', (done) => {
109+
const request = mockRequest({
110+
headers: {},
111+
tokenPayload: undefined,
112+
walletPayload: undefined,
113+
});
114+
115+
interceptor
116+
.intercept(createMockContext(request), mockCallHandler)
117+
.subscribe(() => {
118+
done();
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)