Skip to content

Commit 6f083b1

Browse files
authored
Merge branch 'CS3219-AY2526Sem1:develop' into develop
2 parents 882dcda + c82367b commit 6f083b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+15496
-5215
lines changed

ai/usage-log.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Date/Time: 2025-10-24
2+
# Tool: Claude 4.5 Sonnet (Anthropic)
3+
# Prompt/Command: Requested for Claude to look through the code for all Controller and Route files and generate test cases.
4+
# Output Summary: Corresponding _.test.ts
5+
# Action Taken:
6+
- [ ] Accepted as-is
7+
- [ X ] Modified
8+
- [ ] Rejected
9+
# Author Notes: Correctness was verified by running Jest. Some `expect` logic was incorrect and modified where appropriate.
10+
11+
# Date/Time: 2025-10-25
12+
# Tool: Claude 4.5 Sonnet (Anthropic)
13+
# Prompt/Command: Requested for Claude to look through the code for all user-service frontend code and generate test cases.
14+
# Output Summary: Corresponding _.test.ts
15+
# Action Taken:
16+
- [ ] Accepted as-is
17+
- [ X ] Modified
18+
- [ ] Rejected
19+
# Author Notes: Correctness was verified by running Jest. Some `expect` logic was incorrect and modified where appropriate.

babel.config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Adapted from https://medium.com/@tajircuet/support-of-import-meta-on-vite-jest-typescript-91b49eb0446f
2+
module.exports = {
3+
presets: [
4+
[
5+
'@babel/preset-env',
6+
{ useBuiltIns: 'entry', corejs: '2',
7+
targets: { node: 'current' } },
8+
],
9+
'@babel/preset-typescript',
10+
],
11+
plugins: [
12+
function () {
13+
return {
14+
visitor: {
15+
MetaProperty(path) {
16+
path.replaceWithSourceString('process')
17+
},
18+
},
19+
}
20+
},
21+
],
22+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'test-file-stub';
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
// AI Assistance Disclosure:
2+
// Tool: Claude 4.5 Sonnet (Anthropic)
3+
// Date: 2025-10-25
4+
// Scope: Generated comprehensive test cases
5+
// Author review: Tests validated for correctness and fixed test logic where applicable.
6+
7+
import {render, screen} from '@testing-library/react';
8+
import {BrowserRouter, Route, Routes} from 'react-router-dom';
9+
import AuthContainer from '../../components/authContainer';
10+
import useAuth from '../../hooks/useAuth';
11+
12+
jest.mock('../../hooks/useAuth');
13+
jest.mock('../../components/userMenu', () => {
14+
return function UserMenu() {
15+
return require('react').createElement('div', {'data-testid': 'user-menu'}, 'UserMenu');
16+
};
17+
});
18+
19+
const mockUseAuth = useAuth as jest.Mock;
20+
21+
const renderComponent = (initialRoute = '/') => {
22+
window.history.pushState({}, '', initialRoute);
23+
24+
return render(
25+
<BrowserRouter>
26+
<Routes>
27+
<Route path="/" element={<AuthContainer />}>
28+
<Route index element={<div>Home Content</div>} />
29+
<Route path="profile" element={<div>Profile Content</div>} />
30+
<Route path="settings" element={<div>Settings Content</div>} />
31+
</Route>
32+
<Route path="/home" element={<div>Landing Page</div>} />
33+
<Route path="/complete-profile" element={<div>Complete Profile Page</div>} />
34+
</Routes>
35+
</BrowserRouter>
36+
);
37+
};
38+
39+
const baseUser = {
40+
username: 'testuser',
41+
42+
verified: true,
43+
googleOAuthVerified: false,
44+
githubOAuthVerified: false,
45+
profileComplete: true,
46+
};
47+
48+
describe('components/authContainer', () => {
49+
beforeEach(() => {
50+
jest.clearAllMocks();
51+
});
52+
53+
describe('Loading State', () => {
54+
it('should render loading spinner when isLoading is true', () => {
55+
mockUseAuth.mockReturnValue({user: null, isLoading: true});
56+
const {container} = renderComponent();
57+
58+
expect(container.querySelector('.loading')).toBeInTheDocument();
59+
expect(container.querySelector('.spinner')).toBeInTheDocument();
60+
});
61+
62+
it('should not render UserMenu when loading', () => {
63+
mockUseAuth.mockReturnValue({user: null, isLoading: true});
64+
renderComponent();
65+
66+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
67+
});
68+
69+
it('should not render outlet content when loading', () => {
70+
mockUseAuth.mockReturnValue({user: null, isLoading: true});
71+
renderComponent();
72+
73+
expect(screen.queryByText('Home Content')).not.toBeInTheDocument();
74+
});
75+
});
76+
77+
describe('No User (Unauthenticated)', () => {
78+
it('should redirect to /home when user is null', () => {
79+
mockUseAuth.mockReturnValue({user: null, isLoading: false});
80+
renderComponent('/profile');
81+
82+
expect(screen.getByText('Landing Page')).toBeInTheDocument();
83+
});
84+
85+
it('should redirect to /home when user is undefined', () => {
86+
mockUseAuth.mockReturnValue({user: undefined, isLoading: false});
87+
renderComponent('/settings');
88+
89+
expect(screen.getByText('Landing Page')).toBeInTheDocument();
90+
});
91+
92+
it('should not render UserMenu when not authenticated', () => {
93+
mockUseAuth.mockReturnValue({user: null, isLoading: false});
94+
renderComponent();
95+
96+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
97+
});
98+
});
99+
100+
describe('Authenticated User - Profile Complete', () => {
101+
it('should render UserMenu when user is authenticated', () => {
102+
mockUseAuth.mockReturnValue({user: baseUser, isLoading: false});
103+
renderComponent();
104+
105+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
106+
});
107+
108+
it('should render outlet content when user is authenticated', () => {
109+
mockUseAuth.mockReturnValue({user: baseUser, isLoading: false});
110+
renderComponent();
111+
112+
expect(screen.getByText('Home Content')).toBeInTheDocument();
113+
});
114+
115+
it('should render container div with correct class', () => {
116+
mockUseAuth.mockReturnValue({user: baseUser, isLoading: false});
117+
const {container} = renderComponent();
118+
119+
expect(container.querySelector('.container')).toBeInTheDocument();
120+
});
121+
});
122+
123+
describe('Profile Incomplete', () => {
124+
it('should redirect to /complete-profile when profileComplete is false', () => {
125+
const incompleteUser = {...baseUser, profileComplete: false};
126+
mockUseAuth.mockReturnValue({user: incompleteUser, isLoading: false});
127+
renderComponent('/profile');
128+
129+
expect(screen.getByText('Complete Profile Page')).toBeInTheDocument();
130+
});
131+
132+
it('should not redirect when already on /complete-profile', () => {
133+
const incompleteUser = {...baseUser, profileComplete: false};
134+
mockUseAuth.mockReturnValue({user: incompleteUser, isLoading: false});
135+
136+
window.history.pushState({}, '', '/complete-profile');
137+
render(
138+
<BrowserRouter>
139+
<Routes>
140+
<Route path="/complete-profile" element={<AuthContainer />}>
141+
<Route index element={<div>Complete Profile Content</div>} />
142+
</Route>
143+
</Routes>
144+
</BrowserRouter>
145+
);
146+
147+
expect(screen.getByText('Complete Profile Content')).toBeInTheDocument();
148+
});
149+
150+
it('should not render UserMenu when profile is incomplete', () => {
151+
const incompleteUser = {...baseUser, profileComplete: false};
152+
mockUseAuth.mockReturnValue({user: incompleteUser, isLoading: false});
153+
renderComponent('/settings');
154+
155+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
156+
});
157+
});
158+
159+
describe('User Verification', () => {
160+
it('should allow verified user to access any route', () => {
161+
const verifiedUser = {...baseUser, verified: true};
162+
mockUseAuth.mockReturnValue({user: verifiedUser, isLoading: false});
163+
renderComponent('/profile');
164+
165+
expect(screen.getByText('Profile Content')).toBeInTheDocument();
166+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
167+
});
168+
169+
it('should allow Google OAuth verified user to access any route', () => {
170+
const googleUser = {...baseUser, verified: false, googleOAuthVerified: true};
171+
mockUseAuth.mockReturnValue({user: googleUser, isLoading: false});
172+
renderComponent('/settings');
173+
174+
expect(screen.getByText('Settings Content')).toBeInTheDocument();
175+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
176+
});
177+
178+
it('should allow GitHub OAuth verified user to access any route', () => {
179+
const githubUser = {...baseUser, verified: false, githubOAuthVerified: true};
180+
mockUseAuth.mockReturnValue({user: githubUser, isLoading: false});
181+
renderComponent('/profile');
182+
183+
expect(screen.getByText('Profile Content')).toBeInTheDocument();
184+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
185+
});
186+
187+
it('should redirect unverified user to / when trying to access other routes', () => {
188+
const unverifiedUser = {
189+
...baseUser,
190+
verified: false,
191+
googleOAuthVerified: false,
192+
githubOAuthVerified: false,
193+
};
194+
195+
// Set window location before rendering
196+
delete (window as any).location;
197+
(window as any).location = {pathname: '/profile'};
198+
199+
mockUseAuth.mockReturnValue({user: unverifiedUser, isLoading: false});
200+
renderComponent('/');
201+
202+
expect(screen.getByText('Home Content')).toBeInTheDocument();
203+
});
204+
205+
it('should allow unverified user to stay on / route', () => {
206+
const unverifiedUser = {
207+
...baseUser,
208+
verified: false,
209+
googleOAuthVerified: false,
210+
githubOAuthVerified: false,
211+
};
212+
mockUseAuth.mockReturnValue({user: unverifiedUser, isLoading: false});
213+
renderComponent('/');
214+
215+
expect(screen.getByText('Home Content')).toBeInTheDocument();
216+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
217+
});
218+
219+
it('should redirect unverified user from /settings to /', () => {
220+
const unverifiedUser = {
221+
...baseUser,
222+
verified: false,
223+
googleOAuthVerified: false,
224+
githubOAuthVerified: false,
225+
};
226+
227+
// Set window location before rendering
228+
delete (window as any).location;
229+
(window as any).location = {pathname: '/settings'};
230+
231+
mockUseAuth.mockReturnValue({user: unverifiedUser, isLoading: false});
232+
renderComponent('/');
233+
234+
expect(screen.getByText('Home Content')).toBeInTheDocument();
235+
});
236+
});
237+
238+
describe('Combined Scenarios', () => {
239+
it('should redirect unverified user with incomplete profile to /complete-profile', () => {
240+
const user = {
241+
...baseUser,
242+
verified: false,
243+
googleOAuthVerified: false,
244+
githubOAuthVerified: false,
245+
profileComplete: false,
246+
};
247+
mockUseAuth.mockReturnValue({user, isLoading: false});
248+
renderComponent('/profile');
249+
250+
expect(screen.getByText('Complete Profile Page')).toBeInTheDocument();
251+
});
252+
253+
it('should prioritize profile completion over verification', () => {
254+
const user = {
255+
...baseUser,
256+
verified: false,
257+
googleOAuthVerified: false,
258+
githubOAuthVerified: false,
259+
profileComplete: false,
260+
};
261+
mockUseAuth.mockReturnValue({user, isLoading: false});
262+
renderComponent('/settings');
263+
264+
// Should redirect to complete-profile, not to /
265+
expect(screen.getByText('Complete Profile Page')).toBeInTheDocument();
266+
expect(screen.queryByText('Home Content')).not.toBeInTheDocument();
267+
});
268+
269+
it('should handle verified user with complete profile on any route', () => {
270+
const user = {...baseUser, verified: true, profileComplete: true};
271+
mockUseAuth.mockReturnValue({user, isLoading: false});
272+
renderComponent('/settings');
273+
274+
expect(screen.getByText('Settings Content')).toBeInTheDocument();
275+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
276+
});
277+
});
278+
279+
describe('Edge Cases', () => {
280+
it('should handle user with all verification methods', () => {
281+
const fullyVerifiedUser = {
282+
...baseUser,
283+
verified: true,
284+
googleOAuthVerified: true,
285+
githubOAuthVerified: true,
286+
};
287+
mockUseAuth.mockReturnValue({user: fullyVerifiedUser, isLoading: false});
288+
renderComponent('/profile');
289+
290+
expect(screen.getByText('Profile Content')).toBeInTheDocument();
291+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
292+
});
293+
294+
it('should handle transition from loading to authenticated', () => {
295+
// First render: loading state
296+
mockUseAuth.mockReturnValue({user: null, isLoading: true});
297+
298+
const {container} = renderComponent('/');
299+
300+
// Check loading spinner is present
301+
expect(container.querySelector('.spinner')).toBeInTheDocument();
302+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
303+
304+
// Clean up first render
305+
container.remove();
306+
307+
// Second render: authenticated state
308+
mockUseAuth.mockReturnValue({user: baseUser, isLoading: false});
309+
310+
renderComponent('/');
311+
312+
// Check authenticated content is present
313+
expect(screen.queryByText('Home Content')).toBeInTheDocument();
314+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
315+
});
316+
});
317+
});

0 commit comments

Comments
 (0)