Skip to content

Commit ad2d301

Browse files
authored
Merge pull request #109 from codegasms/shubh/jestreport
Setup testing with jest
2 parents 9c8509e + 3d5bf82 commit ad2d301

File tree

11 files changed

+1576
-251
lines changed

11 files changed

+1576
-251
lines changed

bun.lock

Lines changed: 852 additions & 247 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jest.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
preset: 'ts-jest',
5+
testEnvironment: 'jsdom',
6+
transform: {
7+
'^.+\\.(ts|tsx)$': 'ts-jest',
8+
},
9+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
10+
moduleNameMapper: {
11+
'^@/(.*)$': '<rootDir>/$1', // Changed from src/$1 to match tsconfig
12+
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
13+
'^.+\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',
14+
},
15+
transformIgnorePatterns: ['<rootDir>/node_modules/'],
16+
modulePaths: ['<rootDir>'],
17+
roots: ['<rootDir>'],
18+
};
19+
20+
export default config;

jest.setup.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// jest.setup.js
2+
import '@testing-library/jest-dom';
3+
import { TextEncoder, TextDecoder } from 'util';
4+
import { fetch, Headers, Request, Response } from 'cross-fetch';
5+
6+
// Define the global objects needed for Next.js
7+
global.TextEncoder = TextEncoder;
8+
global.TextDecoder = TextDecoder as any;
9+
global.fetch = fetch;
10+
global.Headers = Headers;
11+
global.Request = Request;
12+
global.Response = Response;

package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"dev": "next dev",
88
"build": "next build",
99
"start": "next start",
10+
"test": "jest --watch",
11+
"test:ci": "jest --coverage --ci",
1012
"lint": "next lint",
1113
"format": "prettier --write \"**/*.{ts,tsx}\"",
1214
"format:check": "prettier --check \"**/*.{ts,tsx}\"",
@@ -21,7 +23,6 @@
2123
"email": "tsx scripts/email.ts"
2224
},
2325
"dependencies": {
24-
"@anatine/zod-openapi": "^2.2.8",
2526
"@codemirror/lang-cpp": "^6.0.2",
2627
"@codemirror/lang-java": "^6.0.1",
2728
"@codemirror/lang-javascript": "6.2.2",
@@ -49,12 +50,17 @@
4950
"@radix-ui/react-toast": "^1.2.2",
5051
"@radix-ui/react-tooltip": "^1.1.4",
5152
"@tabler/icons-react": "^3.24.0",
53+
"@testing-library/jest-dom": "^6.6.3",
54+
"@testing-library/react": "^16.3.0",
55+
"@testing-library/user-event": "^14.6.1",
5256
"@types/bcryptjs": "^2.4.6",
57+
"@types/jest": "^29.5.14",
5358
"@types/js-cookie": "^3.0.6",
5459
"@types/swagger-ui-react": "^5.18.0",
5560
"@uiw/codemirror-themes-all": "^4.23.6",
5661
"@uiw/react-codemirror": "^4.23.6",
5762
"@uiw/react-md-editor": "^4.0.4",
63+
"babel-jest": "^29.7.0",
5864
"bcryptjs": "^2.4.3",
5965
"class-variance-authority": "^0.7.1",
6066
"clsx": "^2.1.1",
@@ -64,7 +70,9 @@
6470
"drizzle-orm": "^0.40.0",
6571
"enquirer": "^2.4.1",
6672
"framer-motion": "^11.13.1",
67-
"ioredis": "^5.6.1",
73+
"identity-obj-proxy": "^3.0.0",
74+
"jest": "^29.7.0",
75+
"jest-environment-jsdom": "^29.7.0",
6876
"js-cookie": "^3.0.5",
6977
"lucide-react": "^0.446.0",
7078
"marked": "^15.0.6",
@@ -83,10 +91,11 @@
8391
"react-syntax-highlighter": "^15.5.0",
8492
"recharts": "^2.14.1",
8593
"simplex-noise": "^4.0.3",
86-
"swagger-ui-react": "^5.21.0",
94+
"swagger-ui-react": "^5.20.1",
8795
"tailwind-merge": "^2.5.5",
8896
"tailwindcss-animate": "^1.0.7",
89-
"zod": "^3.23.8"
97+
"ts-jest": "^29.3.2",
98+
"zod": "^3.24.3"
9099
},
91100
"devDependencies": {
92101
"@radix-ui/react-label": "^2.1.0",
@@ -101,6 +110,7 @@
101110
"@types/slate": "^0.47.16",
102111
"@types/slate-react": "^0.50.1",
103112
"@udecode/plate": "^40.2.9",
113+
"cross-fetch": "^4.1.0",
104114
"drizzle-kit": "^0.30.5",
105115
"eslint": "^8",
106116
"eslint-config-next": "14.2.13",
@@ -110,6 +120,7 @@
110120
"slate": "^0.110.2",
111121
"slate-react": "^0.111.0",
112122
"tailwindcss": "^3.4.1",
123+
"ts-node": "^10.9.2",
113124
"tsx": "^4.19.3",
114125
"typescript": "^5"
115126
}

tests/code.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { POST } from '@/app/api/code/route'; // adjust the path if needed
2+
import { NextResponse } from 'next/server';
3+
4+
// Mock global fetch
5+
global.fetch = jest.fn();
6+
7+
jest.mock('next/server', () => ({
8+
NextResponse: {
9+
json: jest.fn((data, init) => ({ data, status: init?.status || 200 })),
10+
},
11+
}));
12+
13+
describe('POST /api/execute', () => {
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it('should forward the request to Piston API and return the result', async () => {
19+
const mockBody = {
20+
language: 'python',
21+
version: '3.10.0',
22+
files: [{ name: 'main.py', content: 'print("Hello")' }],
23+
stdin: '',
24+
args: [],
25+
compile_timeout: 10000,
26+
run_timeout: 3000,
27+
};
28+
29+
const mockPistonResponse = {
30+
run: { stdout: 'Hello\n', stderr: '', code: 0 },
31+
};
32+
33+
(fetch as jest.Mock).mockResolvedValueOnce({
34+
json: jest.fn().mockResolvedValue(mockPistonResponse),
35+
});
36+
37+
const mockRequest = {
38+
json: jest.fn().mockResolvedValue(mockBody),
39+
} as unknown as Request;
40+
41+
const response = await POST(mockRequest);
42+
43+
expect(mockRequest.json).toHaveBeenCalled();
44+
expect(fetch).toHaveBeenCalledWith(
45+
'https://emkc.org/api/v2/piston/execute',
46+
expect.objectContaining({
47+
method: 'POST',
48+
headers: { 'Content-Type': 'application/json' },
49+
body: JSON.stringify(mockBody),
50+
}),
51+
);
52+
expect(response.status).toBe(200);
53+
expect(response.data).toEqual(mockPistonResponse);
54+
});
55+
56+
it('should return 500 on unexpected error', async () => {
57+
const mockRequest = {
58+
json: jest.fn().mockRejectedValue(new Error('Invalid JSON')),
59+
} as unknown as Request;
60+
61+
const response = await POST(mockRequest);
62+
63+
expect(response.status).toBe(500);
64+
expect(response.data).toEqual({ error: 'Failed to execute code' });
65+
});
66+
});

tests/health.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { GET } from '@/app/api/health/route';
2+
import { sql } from 'drizzle-orm';
3+
4+
jest.mock('@/db/drizzle', () => ({
5+
db: {
6+
execute: jest.fn(),
7+
},
8+
}));
9+
10+
jest.mock('next/server', () => ({
11+
NextResponse: {
12+
json: jest.fn((data, init) => ({ data, status: init?.status || 200 })),
13+
},
14+
}));
15+
16+
import { db } from '@/db/drizzle';
17+
import { NextResponse } from 'next/server';
18+
19+
describe('GET /api/health', () => {
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
});
23+
24+
it('should return 200 if DB connection is healthy', async () => {
25+
(db.execute as jest.Mock).mockResolvedValueOnce(undefined);
26+
27+
const res = await GET();
28+
29+
expect(db.execute).toHaveBeenCalledWith(sql`SELECT 1`);
30+
expect(res.status).toBe(200);
31+
expect(res.data).toHaveProperty('status', 'healthy');
32+
expect(res.data).toHaveProperty('timestamp');
33+
});
34+
35+
it('should return 503 if DB connection fails', async () => {
36+
(db.execute as jest.Mock).mockRejectedValueOnce(new Error('Connection error'));
37+
38+
const res = await GET();
39+
40+
expect(db.execute).toHaveBeenCalled();
41+
expect(res.status).toBe(503);
42+
expect(res.data).toMatchObject({
43+
status: 'unhealthy',
44+
error: 'Connection error',
45+
});
46+
expect(res.data).toHaveProperty('timestamp');
47+
});
48+
});

tests/login.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { jest } from '@jest/globals';
2+
3+
// Mock Next.js modules
4+
jest.mock('next/server', () => ({
5+
NextResponse: {
6+
json: jest.fn((data, options) => {
7+
return {
8+
data,
9+
status: options?.status || 200,
10+
headers: new Map(),
11+
};
12+
}),
13+
redirect: jest.fn(),
14+
next: jest.fn(),
15+
},
16+
}));
17+
18+
jest.mock('@/db/drizzle', () => ({
19+
db: {
20+
query: {
21+
users: {
22+
findFirst: jest.fn(),
23+
},
24+
},
25+
},
26+
}));
27+
28+
jest.mock('@/lib/password', () => ({
29+
verifyPassword: jest.fn(),
30+
}));
31+
32+
jest.mock('@/lib/server/session', () => ({
33+
generateSessionToken: jest.fn().mockReturnValue('mock-session-token'),
34+
createSession: jest.fn().mockResolvedValue({
35+
token: 'mock-session-token',
36+
expiresAt: new Date(Date.now() + 86400000)
37+
}),
38+
}));
39+
40+
jest.mock('@/lib/server/cookies', () => ({
41+
setSessionTokenCookie: jest.fn(),
42+
}));
43+
44+
import { NextResponse } from 'next/server';
45+
import { db } from '@/db/drizzle';
46+
import { verifyPassword } from '@/lib/password';
47+
import { generateSessionToken, createSession } from '@/lib/server/session';
48+
import { setSessionTokenCookie } from '@/lib/server/cookies';
49+
import { POST } from '@/app/api/auth/login/route';
50+
51+
describe('POST /api/login', () => {
52+
beforeEach(() => {
53+
jest.clearAllMocks();
54+
});
55+
56+
it('should return user data on valid credentials', async () => {
57+
// Mock user data
58+
const mockUser = {
59+
id: '1',
60+
61+
name: 'Test User',
62+
hashedPassword: 'hashed_password',
63+
};
64+
65+
// Set up mocks
66+
(db.query.users.findFirst as jest.Mock).mockResolvedValue(mockUser);
67+
(verifyPassword as jest.Mock).mockResolvedValue(true);
68+
69+
// Create mock request
70+
const request = {
71+
json: jest.fn().mockResolvedValue({
72+
73+
password: 'password123',
74+
}),
75+
} as unknown as Request;
76+
77+
// Call the handler
78+
const response = await POST(request);
79+
80+
// Assertions
81+
expect(db.query.users.findFirst).toHaveBeenCalled();
82+
expect(verifyPassword).toHaveBeenCalledWith('hashed_password', 'password123');
83+
expect(generateSessionToken).toHaveBeenCalled();
84+
expect(createSession).toHaveBeenCalled();
85+
expect(setSessionTokenCookie).toHaveBeenCalled();
86+
87+
expect(response.data).toEqual({
88+
_id: '1',
89+
90+
name: 'Test User',
91+
});
92+
});
93+
94+
it('should return 401 if user not found', async () => {
95+
// Set up mocks
96+
(db.query.users.findFirst as jest.Mock).mockResolvedValue(null);
97+
98+
// Create mock request
99+
const request = {
100+
json: jest.fn().mockResolvedValue({
101+
102+
password: 'password123',
103+
}),
104+
} as unknown as Request;
105+
106+
// Call the handler
107+
const response = await POST(request);
108+
109+
// Assertions
110+
expect(response.status).toBe(401);
111+
expect(response.data).toEqual({ error: 'Invalid credentials' });
112+
});
113+
114+
it('should return 401 on invalid password', async () => {
115+
// Mock user data
116+
const mockUser = {
117+
id: '1',
118+
119+
name: 'Test User',
120+
hashedPassword: 'hashed_password',
121+
};
122+
123+
// Set up mocks
124+
(db.query.users.findFirst as jest.Mock).mockResolvedValue(mockUser);
125+
(verifyPassword as jest.Mock).mockResolvedValue(false);
126+
127+
// Create mock request
128+
const request = {
129+
json: jest.fn().mockResolvedValue({
130+
131+
password: 'wrong_password',
132+
}),
133+
} as unknown as Request;
134+
135+
// Call the handler
136+
const response = await POST(request);
137+
138+
// Assertions
139+
expect(verifyPassword).toHaveBeenCalled();
140+
expect(response.status).toBe(401);
141+
expect(response.data).toEqual({ error: 'Invalid credentials' });
142+
});
143+
144+
it('should return 400 on invalid request', async () => {
145+
// Create mock request that throws an error when trying to parse JSON
146+
const request = {
147+
json: jest.fn().mockRejectedValue(new Error('Invalid JSON')),
148+
} as unknown as Request;
149+
150+
// Call the handler
151+
const response = await POST(request);
152+
153+
// Assertions
154+
expect(response.status).toBe(400);
155+
expect(response.data).toEqual({ error: 'Invalid request' });
156+
});
157+
});

0 commit comments

Comments
 (0)