Skip to content

Commit b8dd856

Browse files
josunday002jhayniffy
authored andcommitted
test(auth): add controller unit tests with mocked service
Cover login, register, requestChallenge, verifyChallenge endpoints with mocked AuthService and WalletAuthService. Tests verify delegation, validation, network mismatch handling, and error propagation.
1 parent 79f9258 commit b8dd856

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
3+
import { AuthController } from './auth.controller';
4+
import { AuthService } from './auth.service';
5+
import { WalletAuthService } from './wallet-auth.service';
6+
7+
describe('AuthController', () => {
8+
let controller: AuthController;
9+
let authService: jest.Mocked<Pick<AuthService, 'validateStellarAddress' | 'createSession'>>;
10+
let walletAuthService: jest.Mocked<Pick<WalletAuthService, 'createChallenge' | 'verifyAndIssueToken'>>;
11+
12+
const validAddress = `G${'A'.repeat(55)}`;
13+
14+
beforeEach(async () => {
15+
authService = {
16+
validateStellarAddress: jest.fn().mockReturnValue(true),
17+
createSession: jest.fn().mockResolvedValue({
18+
userId: validAddress,
19+
token: Buffer.from(validAddress).toString('base64'),
20+
}),
21+
};
22+
23+
walletAuthService = {
24+
createChallenge: jest.fn().mockResolvedValue({
25+
nonce: 'abc123',
26+
expiresAt: new Date('2026-12-31'),
27+
}),
28+
verifyAndIssueToken: jest.fn().mockResolvedValue({
29+
access_token: 'jwt-token',
30+
token_type: 'Bearer',
31+
}),
32+
};
33+
34+
const module: TestingModule = await Test.createTestingModule({
35+
controllers: [AuthController],
36+
providers: [
37+
{ provide: AuthService, useValue: authService },
38+
{ provide: WalletAuthService, useValue: walletAuthService },
39+
],
40+
}).compile();
41+
42+
controller = module.get(AuthController);
43+
});
44+
45+
describe('login', () => {
46+
it('creates a session for a valid address', async () => {
47+
const result = await controller.login({ address: validAddress });
48+
49+
expect(authService.validateStellarAddress).toHaveBeenCalledWith(validAddress);
50+
expect(authService.createSession).toHaveBeenCalledWith(validAddress);
51+
expect(result).toEqual({
52+
userId: validAddress,
53+
token: expect.any(String),
54+
});
55+
});
56+
57+
it('throws BadRequestException for invalid address', async () => {
58+
authService.validateStellarAddress.mockReturnValue(false);
59+
60+
await expect(controller.login({ address: 'invalid' })).rejects.toThrow(
61+
BadRequestException,
62+
);
63+
expect(authService.createSession).not.toHaveBeenCalled();
64+
});
65+
66+
it('throws BadRequestException when address is omitted', async () => {
67+
authService.validateStellarAddress.mockReturnValue(false);
68+
69+
await expect(controller.login({})).rejects.toThrow(BadRequestException);
70+
});
71+
72+
it('throws on network mismatch', async () => {
73+
await expect(
74+
controller.login({ address: validAddress }, 'mainnet'),
75+
).rejects.toThrow(HttpException);
76+
});
77+
78+
it('passes when x-network matches server network', async () => {
79+
const result = await controller.login({ address: validAddress }, 'testnet');
80+
81+
expect(result).toEqual({
82+
userId: validAddress,
83+
token: expect.any(String),
84+
});
85+
});
86+
87+
it('ignores network header when not provided', async () => {
88+
const result = await controller.login({ address: validAddress }, undefined);
89+
90+
expect(result).toEqual({
91+
userId: validAddress,
92+
token: expect.any(String),
93+
});
94+
});
95+
});
96+
97+
describe('register (deprecated)', () => {
98+
it('creates a session for a valid address', async () => {
99+
const result = await controller.register({ address: validAddress });
100+
101+
expect(authService.createSession).toHaveBeenCalledWith(validAddress);
102+
expect(result).toEqual({
103+
userId: validAddress,
104+
token: expect.any(String),
105+
});
106+
});
107+
108+
it('throws BadRequestException for invalid address', async () => {
109+
authService.validateStellarAddress.mockReturnValue(false);
110+
111+
await expect(controller.register({ address: 'bad' })).rejects.toThrow(
112+
BadRequestException,
113+
);
114+
});
115+
116+
it('throws on network mismatch', async () => {
117+
await expect(
118+
controller.register({ address: validAddress }, 'mainnet'),
119+
).rejects.toThrow(HttpException);
120+
});
121+
});
122+
123+
describe('requestChallenge', () => {
124+
it('returns nonce and expiry for a valid address', async () => {
125+
const result = await controller.requestChallenge({ address: validAddress });
126+
127+
expect(authService.validateStellarAddress).toHaveBeenCalledWith(validAddress);
128+
expect(walletAuthService.createChallenge).toHaveBeenCalledWith(validAddress);
129+
expect(result).toEqual({
130+
nonce: 'abc123',
131+
expiresAt: expect.any(Date),
132+
});
133+
});
134+
135+
it('throws BadRequestException for invalid address', async () => {
136+
authService.validateStellarAddress.mockReturnValue(false);
137+
138+
await expect(
139+
controller.requestChallenge({ address: 'invalid' }),
140+
).rejects.toThrow(BadRequestException);
141+
expect(walletAuthService.createChallenge).not.toHaveBeenCalled();
142+
});
143+
144+
it('throws on network mismatch', async () => {
145+
await expect(
146+
controller.requestChallenge({ address: validAddress }, 'mainnet'),
147+
).rejects.toThrow(HttpException);
148+
});
149+
});
150+
151+
describe('verifyChallenge', () => {
152+
const dto = { address: validAddress, nonce: 'abc123', signature: 'deadbeef' };
153+
154+
it('returns JWT for valid verification', async () => {
155+
const result = await controller.verifyChallenge(dto);
156+
157+
expect(walletAuthService.verifyAndIssueToken).toHaveBeenCalledWith(
158+
validAddress,
159+
'abc123',
160+
'deadbeef',
161+
);
162+
expect(result).toEqual({
163+
access_token: 'jwt-token',
164+
token_type: 'Bearer',
165+
});
166+
});
167+
168+
it('throws BadRequestException for invalid address', async () => {
169+
authService.validateStellarAddress.mockReturnValue(false);
170+
171+
await expect(controller.verifyChallenge(dto)).rejects.toThrow(
172+
BadRequestException,
173+
);
174+
expect(walletAuthService.verifyAndIssueToken).not.toHaveBeenCalled();
175+
});
176+
177+
it('throws on network mismatch', async () => {
178+
await expect(
179+
controller.verifyChallenge(dto, 'mainnet'),
180+
).rejects.toThrow(HttpException);
181+
});
182+
183+
it('propagates service errors', async () => {
184+
walletAuthService.verifyAndIssueToken.mockRejectedValue(
185+
new BadRequestException('Invalid signature'),
186+
);
187+
188+
await expect(controller.verifyChallenge(dto)).rejects.toThrow(
189+
BadRequestException,
190+
);
191+
});
192+
});
193+
194+
describe('assertNetworkMatch (via endpoints)', () => {
195+
it('accepts case-insensitive network match', async () => {
196+
const result = await controller.login({ address: validAddress }, 'Testnet');
197+
expect(result).toBeDefined();
198+
});
199+
200+
it('trims whitespace from network header', async () => {
201+
const result = await controller.login({ address: validAddress }, ' testnet ');
202+
expect(result).toBeDefined();
203+
});
204+
205+
it('includes expected and current network in mismatch error', async () => {
206+
try {
207+
await controller.login({ address: validAddress }, 'mainnet');
208+
fail('should have thrown');
209+
} catch (e) {
210+
expect(e).toBeInstanceOf(HttpException);
211+
const response = (e as HttpException).getResponse();
212+
expect(response).toMatchObject({
213+
error: 'NETWORK_MISMATCH',
214+
expectedNetwork: 'testnet',
215+
currentNetwork: 'mainnet',
216+
});
217+
}
218+
});
219+
});
220+
});

0 commit comments

Comments
 (0)