Skip to content

Commit 3b6efcd

Browse files
authored
AI-603 Backend Unit Tests (#18)
1 parent e9d7313 commit 3b6efcd

16 files changed

Lines changed: 3276 additions & 183 deletions

.github/workflows/backend-qa.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,16 @@ jobs:
2828
node-version: '24'
2929
- name: Install dependencies
3030
run: npm ci
31+
# Prisma-generated types needed
32+
# ENV is just so this will run
33+
- name: Generate Prisma files
34+
working-directory: apps/backend-services
35+
run: npm run db:generate
36+
env:
37+
DATABASE_URL: "file:dev.db"
3138
- name: Run tests
3239
working-directory: apps/backend-services
33-
run: npm test
40+
run: npm run test:cov
3441
- name: Run Linter
3542
working-directory: apps/backend-services
3643
run: npm run lint

apps/backend-services/biome.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,37 @@
119119
}
120120
}
121121
}
122+
},
123+
{
124+
"includes": ["**/*.spec.ts"],
125+
"linter": {
126+
"rules": {
127+
"correctness": {
128+
"noUndeclaredVariables": "off"
129+
},
130+
"suspicious": {
131+
"noExplicitAny": "off",
132+
"noEmptyBlockStatements": "off"
133+
},
134+
"security": {
135+
"noSecrets": "off"
136+
}
137+
}
138+
}
139+
},
140+
{
141+
"includes": ["**/testUtils/*.*"],
142+
"linter": {
143+
"rules": {
144+
"correctness": {
145+
"noUndeclaredVariables": "off"
146+
},
147+
"suspicious": {
148+
"noExplicitAny": "off",
149+
"noEmptyBlockStatements": "off"
150+
}
151+
}
152+
}
122153
}
123154
],
124155
"assist": {

apps/backend-services/package.json

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"devDependencies": {
4949
"@nestjs/cli": "^11.0.13",
5050
"@nestjs/schematics": "^11.0.9",
51-
"@nestjs/testing": "^11.1.9",
51+
"@nestjs/testing": "11.1.9",
5252
"@swc/cli": "^0.7.9",
5353
"@swc/core": "^1.15.3",
5454
"@types/jest": "30.0.0",
@@ -80,9 +80,30 @@
8080
"^.+\\.(t|j)s$": "ts-jest"
8181
},
8282
"collectCoverageFrom": [
83-
"**/*.(t|j)s"
83+
"**/*.(t|j)s",
84+
"!**/*.spec.ts",
85+
"!**/node_modules/**",
86+
"!**/generated/**",
87+
"!main.ts"
8488
],
8589
"coverageDirectory": "../coverage",
86-
"testEnvironment": "node"
90+
"testEnvironment": "node",
91+
"transformIgnorePatterns": [
92+
"node_modules/(?!uuid)"
93+
],
94+
"moduleNameMapper": {
95+
"^@/(.*)$": "<rootDir>/$1"
96+
},
97+
"setupFiles": [
98+
"./testUtils/testSetup.ts"
99+
],
100+
"coverageThreshold": {
101+
"global": {
102+
"branches": 70,
103+
"functions": 70,
104+
"lines": 70,
105+
"statements": 70
106+
}
107+
}
87108
}
88109
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { Test, TestingModule } from "@nestjs/testing";
2+
import { Response } from "express";
3+
import { AuthController } from "./auth.controller";
4+
import { AuthService } from "./auth.service";
5+
import {
6+
AuthResultQueryDto,
7+
LogoutQueryDto,
8+
OAuthCallbackQueryDto,
9+
RefreshTokenDto,
10+
} from "./dto";
11+
import { Public } from "./public.decorator";
12+
13+
describe("AuthController", () => {
14+
let controller: AuthController;
15+
let authService: jest.Mocked<AuthService>;
16+
let res: jest.Mocked<Response>;
17+
18+
beforeEach(async () => {
19+
authService = {
20+
refreshAccessToken: jest.fn(),
21+
getLoginUrl: jest.fn(),
22+
getLogoutUrl: jest.fn(),
23+
handleCallback: jest.fn(),
24+
buildAuthResultRedirect: jest.fn(),
25+
buildErrorRedirect: jest.fn(),
26+
consumeAuthResult: jest.fn(),
27+
} as any;
28+
res = {
29+
redirect: jest.fn(),
30+
status: jest.fn().mockReturnThis(),
31+
json: jest.fn(),
32+
} as any;
33+
const module: TestingModule = await Test.createTestingModule({
34+
controllers: [AuthController],
35+
providers: [{ provide: AuthService, useValue: authService }],
36+
}).compile();
37+
controller = module.get<AuthController>(AuthController);
38+
});
39+
40+
describe("refreshToken", () => {
41+
it("should return tokens from service", async () => {
42+
const dto: RefreshTokenDto = { refresh_token: "refresh" };
43+
authService.refreshAccessToken.mockResolvedValue({
44+
access_token: "a",
45+
refresh_token: "r",
46+
id_token: "i",
47+
expires_in: 123,
48+
token_type: "Bearer",
49+
});
50+
const result = await controller.refreshToken(dto);
51+
expect(authService.refreshAccessToken).toHaveBeenCalledWith("refresh");
52+
expect(result).toEqual({
53+
access_token: "a",
54+
refresh_token: "r",
55+
id_token: "i",
56+
expires_in: 123,
57+
});
58+
});
59+
});
60+
61+
describe("getLoginUrl", () => {
62+
it("should redirect to login url", async () => {
63+
authService.getLoginUrl.mockReturnValue("http://login");
64+
await controller.getLoginUrl(res);
65+
expect(authService.getLoginUrl).toHaveBeenCalled();
66+
expect(res.redirect).toHaveBeenCalledWith("http://login");
67+
});
68+
69+
it("should return 500 if error", async () => {
70+
authService.getLoginUrl.mockImplementation(() => {
71+
throw new Error();
72+
});
73+
await controller.getLoginUrl(res);
74+
expect(res.status).toHaveBeenCalledWith(500);
75+
expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) });
76+
});
77+
});
78+
79+
describe("logout", () => {
80+
it("should redirect to logout url", async () => {
81+
authService.getLogoutUrl.mockReturnValue("http://logout");
82+
const query: LogoutQueryDto = { id_token_hint: "idtoken" };
83+
await controller.logout(query, res);
84+
expect(authService.getLogoutUrl).toHaveBeenCalledWith("idtoken");
85+
expect(res.redirect).toHaveBeenCalledWith("http://logout");
86+
});
87+
88+
it("should return 500 if error", async () => {
89+
authService.getLogoutUrl.mockImplementation(() => {
90+
throw new Error();
91+
});
92+
await controller.logout({ id_token_hint: "idtoken" }, res);
93+
expect(res.status).toHaveBeenCalledWith(500);
94+
expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) });
95+
});
96+
});
97+
98+
describe("oauthCallback", () => {
99+
it("should handle callback and redirect", async () => {
100+
authService.handleCallback.mockResolvedValue("resultId");
101+
authService.buildAuthResultRedirect.mockReturnValue("/redirect");
102+
const query: OAuthCallbackQueryDto = { code: "c", state: "s" } as any;
103+
await controller.oauthCallback(query, res);
104+
expect(authService.handleCallback).toHaveBeenCalledWith("c", "s");
105+
expect(authService.buildAuthResultRedirect).toHaveBeenCalledWith(
106+
"resultId",
107+
);
108+
expect(res.redirect).toHaveBeenCalledWith("/redirect");
109+
});
110+
111+
it("should redirect to error if exception", async () => {
112+
authService.handleCallback.mockRejectedValue(new Error("fail"));
113+
authService.buildErrorRedirect.mockReturnValue("/error");
114+
const query: OAuthCallbackQueryDto = { code: "c", state: "s" } as any;
115+
await controller.oauthCallback(query, res);
116+
expect(authService.buildErrorRedirect).toHaveBeenCalledWith(
117+
"callback_failed",
118+
);
119+
expect(res.redirect).toHaveBeenCalledWith("/error");
120+
});
121+
});
122+
123+
describe("consumeResult", () => {
124+
it("should return result from service", async () => {
125+
authService.consumeAuthResult.mockReturnValue({
126+
access_token: "token",
127+
expires_in: 3600,
128+
token_type: "Bearer",
129+
});
130+
const query: AuthResultQueryDto = { result: "uuid" };
131+
const result = await controller.consumeResult(query);
132+
expect(authService.consumeAuthResult).toHaveBeenCalledWith("uuid");
133+
expect(result).toEqual({
134+
access_token: "token",
135+
expires_in: 3600,
136+
token_type: "Bearer",
137+
});
138+
});
139+
});
140+
});

0 commit comments

Comments
 (0)