From 5253e458d97f373c8309f249ae21c46deef18390 Mon Sep 17 00:00:00 2001 From: verakohh Date: Thu, 13 Nov 2025 04:49:31 +0800 Subject: [PATCH 1/7] feat(frontend): add initial history dashboards UI and logic --- frontend/src/components/userMenu.tsx | 4 +- .../features/progress/RecentSessionsList.tsx | 157 +++- frontend/src/pages/HistoryDashboardPage.tsx | 36 +- frontend/src/pages/QuestionDetailPage.tsx | 36 +- frontend/src/pages/ResetQuestionsPage.tsx | 36 +- frontend/src/pages/profile.tsx | 94 ++- frontend/styles/profile.css | 4 +- .../src/test/models/oAuthLink 2.test.ts | 242 ++++++ .../src/test/models/session 2.test.ts | 221 ++++++ user-service/src/test/models/user 2.test.ts | 737 ++++++++++++++++++ .../test/models/verificationCode 2.test.ts | 431 ++++++++++ 11 files changed, 1922 insertions(+), 76 deletions(-) create mode 100644 user-service/src/test/models/oAuthLink 2.test.ts create mode 100644 user-service/src/test/models/session 2.test.ts create mode 100644 user-service/src/test/models/user 2.test.ts create mode 100644 user-service/src/test/models/verificationCode 2.test.ts diff --git a/frontend/src/components/userMenu.tsx b/frontend/src/components/userMenu.tsx index 10a23cd9b8..9803b58474 100644 --- a/frontend/src/components/userMenu.tsx +++ b/frontend/src/components/userMenu.tsx @@ -53,9 +53,9 @@ const UserMenu: React.FC = () => { ) : ( <> - + */} {isVerified ? ( diff --git a/frontend/src/features/progress/RecentSessionsList.tsx b/frontend/src/features/progress/RecentSessionsList.tsx index 009754daa3..b4eb81b671 100644 --- a/frontend/src/features/progress/RecentSessionsList.tsx +++ b/frontend/src/features/progress/RecentSessionsList.tsx @@ -1,17 +1,15 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { getAllAttemptSummaries } from '../../lib/api'; +import { getAllAttemptSummaries, getOtherUser } from '../../lib/api'; // --- FIX: Import both time formatters --- import { formatDuration, formatTimeAgo } from '../../lib/timeFormatters'; -// import useAuth from '../hooks/useAuth'; // Added useAuth to get the user // --- UI Imports from New UI --- import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { - User, - Clock, - Calendar // Added for "time ago" + Calendar, + Code } from "lucide-react"; // ------------------------------------ @@ -22,17 +20,19 @@ interface SessionSummary { question_title: string; question_difficulty: "Easy" | "Medium" | "Hard"; partner_id: string; - is_solved_successfully: boolean; + is_solved_successfully: boolean | null; has_penalty: boolean; // We need this to determine "Incomplete" started_at: string; - time_taken_ms: number; + time_taken_ms: number | null; } const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userId, limit }) => { const navigate = useNavigate(); const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); + const [usernames, setUsernames] = useState>(new Map()); + // Fetch sessions useEffect(() => { if (!userId) return; @@ -53,6 +53,39 @@ const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userI fetchRecent(); }, [userId, limit]); + // Fetch usernames for all unique partner IDs + useEffect(() => { + if (sessions.length === 0) return; + + const fetchUsernames = async () => { + const uniquePartnerIds = [...new Set(sessions.map(s => s.partner_id).filter(id => id))]; + const usernameMap = new Map(); + + // Fetch usernames in parallel + await Promise.allSettled( + uniquePartnerIds.map(async (partnerId) => { + try { + const response = await getOtherUser(partnerId); + // getOtherUser returns { data: { username, ... } } + if (response?.data?.username) { + usernameMap.set(partnerId, response.data.username); + } else { + usernameMap.set(partnerId, partnerId); + } + } catch (error) { + console.error(`Failed to fetch username for partner ${partnerId}:`, error); + // Keep partner_id as fallback + usernameMap.set(partnerId, partnerId); + } + }) + ); + + setUsernames(usernameMap); + }; + + fetchUsernames(); + }, [sessions]); + const handleSessionClick = (questionId: string) => { // Navigate to the detail page for that question navigate(`/history/attempts/${questionId}`); @@ -68,56 +101,100 @@ const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userI // --- This is the new UI, adapted to the REAL API data --- return ( -
+
{/* The title "Recent Sessions" is in your userProfile.tsx, so we don't repeat it here */} -
- {sessions.map((session) => ( + {sessions.map((session) => { + const statusText = session.is_solved_successfully === true ? "Passed" : (session.has_penalty ? "Incomplete" : "Failed"); + const statusColor = session.is_solved_successfully === true ? 'bg-green-500' : (session.has_penalty ? 'bg-orange-500' : 'bg-red-500'); + const difficultyColors: Record = { + Easy: { bg: 'bg-green-100', text: 'text-green-800' }, + Medium: { bg: 'bg-yellow-100', text: 'text-yellow-800' }, + Hard: { bg: 'bg-red-100', text: 'text-red-800' } + }; + const difficultyStyle = difficultyColors[session.question_difficulty] || difficultyColors.Medium; + + return ( { + e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)'; + }} onClick={() => handleSessionClick(session.question_id)} > -
-
-
+
+ {/* Left: Code Icon */} +
+ +
+ + {/* Middle: Topic, Difficulty, Partner, Time */} +
+ {/* Topic in bold */} +
+ {session.question_title} +
+ + {/* Difficulty badge and Partner */} +
{session.question_difficulty} - {/* API provides question_title, not topic */} - {session.question_title} + + with {usernames.get(session.partner_id) || session.partner_id} +
-
-
- - {session.partner_id} -
+ + {/* Time taken and Time ago */} +
+ {session.time_taken_ms && ( + {formatDuration(session.time_taken_ms)} + )}
- - {/* API provides time_taken_ms, format it */} - {formatDuration(session.time_taken_ms)} -
- {/* API provides started_at, let's show time ago */} -
- {formatTimeAgo(session.started_at)} + {formatTimeAgo(session.started_at)}
- - {/* API provides booleans, we derive the status */} - - {session.is_solved_successfully ? "Passed" : (session.has_penalty ? "Incomplete" : "Failed")} - + + {/* Right: Status Badge */} +
+ + {statusText} + +
- ))} -
+ ); + })}
); }; diff --git a/frontend/src/pages/HistoryDashboardPage.tsx b/frontend/src/pages/HistoryDashboardPage.tsx index 239e32cd0e..fff9fdbc30 100644 --- a/frontend/src/pages/HistoryDashboardPage.tsx +++ b/frontend/src/pages/HistoryDashboardPage.tsx @@ -15,7 +15,7 @@ import { // --- Logic Imports (from Cursor's file) --- import useAuth from '../hooks/useAuth'; -import { getHistoryProgress, getAllAttemptSummaries } from '../lib/api'; +import { getHistoryProgress, getAllAttemptSummaries, getOtherUser } from '../lib/api'; import { formatTimeAgo } from '../lib/timeFormatters'; // --- Main Component --- @@ -28,6 +28,7 @@ export default function HistoryDashboardPage() { const [progress, setProgress] = useState(null); const [summaries, setSummaries] = useState([]); const [loading, setLoading] = useState(true); + const [usernames, setUsernames] = useState>(new Map()); // --- Data Fetching (from Cursor's file) --- useEffect(() => { @@ -49,6 +50,37 @@ export default function HistoryDashboardPage() { }); }, [userId]); + // Fetch usernames for all unique partner IDs + useEffect(() => { + if (summaries.length === 0) return; + + const fetchUsernames = async () => { + const uniquePartnerIds = [...new Set(summaries.map((s: any) => s.partner_id).filter((id: string) => id))]; + const usernameMap = new Map(); + + // Fetch usernames in parallel + await Promise.allSettled( + uniquePartnerIds.map(async (partnerId: string) => { + try { + const response = await getOtherUser(partnerId); + if (response?.data?.username) { + usernameMap.set(partnerId, response.data.username); + } else { + usernameMap.set(partnerId, partnerId); + } + } catch (error) { + console.error(`Failed to fetch username for partner ${partnerId}:`, error); + usernameMap.set(partnerId, partnerId); + } + }) + ); + + setUsernames(usernameMap); + }; + + fetchUsernames(); + }, [summaries]); + // --- Stat Calculation (from Cursor's file, adapted for Lovable's UI) --- const totals = useMemo(() => { const uniqueQuestions = summaries.length; // Renamed to match UI @@ -185,7 +217,7 @@ export default function HistoryDashboardPage() {
- Partner: {question.partner_id} + Partner: {usernames.get(question.partner_id) || question.partner_id} {/* Real data */} diff --git a/frontend/src/pages/QuestionDetailPage.tsx b/frontend/src/pages/QuestionDetailPage.tsx index 0b2dd08b3d..794fcb87e4 100644 --- a/frontend/src/pages/QuestionDetailPage.tsx +++ b/frontend/src/pages/QuestionDetailPage.tsx @@ -19,7 +19,7 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; // --- API & Auth Imports (from Logic) --- import useAuth from '../hooks/useAuth'; -import { getQuestionAttempts } from '../lib/api'; +import { getQuestionAttempts, getOtherUser } from '../lib/api'; import { formatDurationMinutes } from '../lib/timeFormatters'; // ... (interface Attempt definition is correct) ... @@ -45,6 +45,7 @@ export const QuestionDetail = () => { const userId = (user as any)?._id ?? (user as any)?.uid ?? ''; const [attempts, setAttempts] = useState([]); const [loading, setLoading] = useState(true); + const [usernames, setUsernames] = useState>(new Map()); useEffect(() => { if (!userId || !questionId) return; @@ -62,6 +63,37 @@ export const QuestionDetail = () => { }); }, [userId, questionId]); + // Fetch usernames for all unique partner IDs + useEffect(() => { + if (attempts.length === 0) return; + + const fetchUsernames = async () => { + const uniquePartnerIds = [...new Set(attempts.map(a => a.partner_id).filter(id => id))]; + const usernameMap = new Map(); + + // Fetch usernames in parallel + await Promise.allSettled( + uniquePartnerIds.map(async (partnerId: string) => { + try { + const response = await getOtherUser(partnerId); + if (response?.data?.username) { + usernameMap.set(partnerId, response.data.username); + } else { + usernameMap.set(partnerId, partnerId); + } + } catch (error) { + console.error(`Failed to fetch username for partner ${partnerId}:`, error); + usernameMap.set(partnerId, partnerId); + } + }) + ); + + setUsernames(usernameMap); + }; + + fetchUsernames(); + }, [attempts]); + const questionTitle = attempts[0]?.question_title; // --- Helper function to guess language --- @@ -164,7 +196,7 @@ export const QuestionDetail = () => {
- Partner: {attempt.partner_id} + Partner: {usernames.get(attempt.partner_id) || attempt.partner_id} diff --git a/frontend/src/pages/ResetQuestionsPage.tsx b/frontend/src/pages/ResetQuestionsPage.tsx index 209f569879..500388e1b8 100644 --- a/frontend/src/pages/ResetQuestionsPage.tsx +++ b/frontend/src/pages/ResetQuestionsPage.tsx @@ -420,7 +420,7 @@ import { useToast } from "@/hooks/use-toast"; // --- API & Auth Imports --- import useAuth from '../hooks/useAuth'; -import { getTopics, getActiveAttempts, getAllAttemptSummaries, resetQuestions } from '../lib/api'; +import { getTopics, getActiveAttempts, getAllAttemptSummaries, resetQuestions, getOtherUser } from '../lib/api'; import { formatDuration } from '../lib/timeFormatters'; // --- Types (from API) --- @@ -467,6 +467,7 @@ const ResetQuestions = () => { const [searchQuery, setSearchQuery] = useState(""); const [filterTopic, setFilterTopic] = useState("all"); const [filterDifficulty, setFilterDifficulty] = useState("all"); + const [usernames, setUsernames] = useState>(new Map()); const [isAlertOpen, setIsAlertOpen] = useState(false); @@ -497,6 +498,37 @@ const ResetQuestions = () => { return matchesSearch && matchesTopic && matchesDifficulty; }); + // Fetch usernames for all unique partner IDs in filtered questions + useEffect(() => { + if (filteredQuestions.length === 0) return; + + const fetchUsernames = async () => { + const uniquePartnerIds = [...new Set(filteredQuestions.map(q => q.partner_id).filter(id => id))]; + const usernameMap = new Map(); + + // Fetch usernames in parallel + await Promise.allSettled( + uniquePartnerIds.map(async (partnerId: string) => { + try { + const response = await getOtherUser(partnerId); + if (response?.data?.username) { + usernameMap.set(partnerId, response.data.username); + } else { + usernameMap.set(partnerId, partnerId); + } + } catch (error) { + console.error(`Failed to fetch username for partner ${partnerId}:`, error); + usernameMap.set(partnerId, partnerId); + } + }) + ); + + setUsernames(usernameMap); + }; + + fetchUsernames(); + }, [filteredQuestions]); + // --- Event Handlers --- const handleQuestionToggle = (questionId: string, checked: boolean) => { if (checked) { @@ -784,7 +816,7 @@ const ResetQuestions = () => {
Time: {formatDuration(question.time_taken_ms)}
{question.partner_id && ( -
Partner: {question.partner_id}
+
Partner: {usernames.get(question.partner_id) || question.partner_id}
)}
diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx index fed9ab7ff2..be1f72c4c9 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -10,13 +10,30 @@ import QuestionSettingIcon from '../assets/profile/setting-icon.svg'; import '../../styles/profile.css'; import useAuth from '../hooks/useAuth'; import {Link, useNavigate} from 'react-router-dom'; -import {resendEmail} from '../lib/api'; +import {resendEmail, getHistoryProgress, getAllAttemptSummaries} from '../lib/api'; +import {useQuery} from '@tanstack/react-query'; +import {formatDuration} from '../lib/timeFormatters'; +import RecentSessionsList from '../features/progress/RecentSessionsList'; const Profile: React.FC = () => { const {user} = useAuth(); const navigate = useNavigate(); const {username, email, verified, googleOAuthVerified, githubOAuthVerified} = user; const isVerified = verified || googleOAuthVerified || githubOAuthVerified; + const userId = (user as any)?._id ?? (user as any)?.uid ?? ''; + + // Fetch user progress and session summaries using useQuery + const {data: progress, isLoading: progressLoading, isError: progressError} = useQuery({ + queryKey: ['historyProgress', userId], + queryFn: () => getHistoryProgress(userId), + enabled: !!userId && isVerified, + }); + + const {data: summaries, isLoading: summariesLoading, isError: summariesError} = useQuery({ + queryKey: ['attemptSummaries', userId], + queryFn: () => getAllAttemptSummaries(userId), + enabled: !!userId && isVerified, + }); const handleResendEmail = async () => { try { @@ -86,34 +103,50 @@ const Profile: React.FC = () => {
-
-
-

0

-

Sessions Completed

-
- Sessions -
-
-
-

0

-

Problems Solved

-
- Problems -
-
-
-

0

-

Hours Practiced

+ {progressLoading ? ( +
+
+

Loading...

+
- Hours -
-
-
-

0

-

Current Streak

+ ) : progressError ? ( +
+
+

Error loading stats

+
- Streak -
+ ) : ( + <> +
+
+

{progress?.total_sessions_completed ?? 0}

+

Sessions Completed

+
+ Sessions +
+
+
+

{progress?.total_successes ?? 0}

+

Problems Solved

+
+ Problems +
+
+
+

{formatDuration(progress?.total_time_ms)}

+

Hours Practiced

+
+ Hours +
+
+
+

{progress?.current_streak ?? 0}

+

Current Streak

+
+ Streak +
+ + )}
@@ -149,6 +182,13 @@ const Profile: React.FC = () => {

Recent Activity

+ {summariesLoading ? ( +
Loading recent sessions...
+ ) : summariesError ? ( +
Error loading recent sessions
+ ) : userId ? ( + + ) : null}
diff --git a/frontend/styles/profile.css b/frontend/styles/profile.css index 9e28a3b877..106d4b8852 100644 --- a/frontend/styles/profile.css +++ b/frontend/styles/profile.css @@ -167,6 +167,7 @@ .actions-activity-section { display: flex; gap: 2rem; + align-items: flex-start; } .quick-actions-container { @@ -180,13 +181,14 @@ display: flex; flex-direction: column; flex: 1; + gap: 1.5rem; } .section-title { font-size: 30px; font-weight: 600; color: #303030; - margin-bottom: 1rem; + margin: 0; } .action-card { diff --git a/user-service/src/test/models/oAuthLink 2.test.ts b/user-service/src/test/models/oAuthLink 2.test.ts new file mode 100644 index 0000000000..2c42ab95af --- /dev/null +++ b/user-service/src/test/models/oAuthLink 2.test.ts @@ -0,0 +1,242 @@ +import {OAUTH_LINK_MINS} from '../../constants/expirables'; +import OAuthLink from '../../models/oAuthLink'; +import User from '../../models/user'; + +describe('models/oAuthLink', () => { + let testUserId; + + beforeEach(async () => { + const user = await User.create({ + username: 'testuser', + email: 'test@example.com', + password: 'password123', + }); + testUserId = user._id; + }); + + describe('Link creation', () => { + it('should create a link with required fields', async () => { + const oAuthLink = await OAuthLink.create({ + userId: testUserId, + }); + + expect(oAuthLink.userId).toEqual(testUserId); + expect(oAuthLink.createdAt).toBeDefined(); + expect(oAuthLink.createdAt).toBeInstanceOf(Date); + expect(oAuthLink.expiresAt).toBeDefined(); + expect(oAuthLink.expiresAt).toBeInstanceOf(Date); + }); + + it('should set default createdAt to current time', async () => { + const beforeCreate = Date.now(); + const oAuthLink = await OAuthLink.create({ + userId: testUserId, + }); + const afterCreate = Date.now(); + + const createdTime = oAuthLink.createdAt.getTime(); + expect(createdTime).toBeGreaterThanOrEqual(beforeCreate); + expect(createdTime).toBeLessThanOrEqual(afterCreate); + }); + + it('should set default expiresAt to OAUTH_LINK_MINS in future', async () => { + const beforeCreate = Date.now(); + const oAuthLink = await OAuthLink.create({ + userId: testUserId, + }); + const afterCreate = Date.now(); + + const expectedMinExpiry = beforeCreate + OAUTH_LINK_MINS * 60 * 1000; + const expectedMaxExpiry = afterCreate + OAUTH_LINK_MINS * 60 * 1000; + const actualExpiry = oAuthLink.expiresAt.getTime(); + + expect(actualExpiry).toBeGreaterThanOrEqual(expectedMinExpiry); + expect(actualExpiry).toBeLessThanOrEqual(expectedMaxExpiry); + }); + + it('should allow custom expiresAt', async () => { + const customExpiry = new Date(Date.now() + 7 * 60 * 1000); + + const oAuthLink = await OAuthLink.create({ + userId: testUserId, + expiresAt: customExpiry, + }); + + expect(oAuthLink.expiresAt.getTime()).toBe(customExpiry.getTime()); + }); + + it('should generate unique link Ids', async () => { + const oAuthLink1 = await OAuthLink.create({userId: testUserId}); + const oAuthLink2 = await OAuthLink.create({userId: testUserId}); + + expect(oAuthLink1._id.toString()).not.toBe(oAuthLink2._id.toString()); + }); + }); + + describe('OAuthLink validation', () => { + it('should require userId', async () => { + const oAuthLinkData = {}; + + await expect(OAuthLink.create(oAuthLinkData)).rejects.toThrow(); + }); + }); + + describe('OAuthLink queries', () => { + beforeEach(async () => { + await OAuthLink.create([ + {userId: testUserId}, + {userId: testUserId}, + { + userId: testUserId, + expiresAt: new Date(Date.now() - 1000), // Expired + }, + ]); + }); + + it('should find all links for a user', async () => { + const oAuthLink = await OAuthLink.find({userId: testUserId}); + + expect(oAuthLink.length).toBe(3); + expect(oAuthLink.every(s => s.userId.equals(testUserId))).toBe(true); + }); + + it('should find links by Id', async () => { + const createdOAuthLink = await OAuthLink.create({userId: testUserId}); + const foundOAuthLink = await OAuthLink.findById(createdOAuthLink._id); + + expect(foundOAuthLink).not.toBeNull(); + expect(foundOAuthLink._id.toString()).toBe(createdOAuthLink._id.toString()); + }); + + it('should find active links (not expired)', async () => { + const now = new Date(); + const activeoAuthLink = await OAuthLink.find({ + userId: testUserId, + expiresAt: {$gt: now}, + }); + + expect(activeoAuthLink.length).toBe(2); + expect(activeoAuthLink.every(s => s.expiresAt > now)).toBe(true); + }); + + it('should find expired links', async () => { + const now = new Date(); + const expiredoAuthLink = await OAuthLink.find({ + userId: testUserId, + expiresAt: {$lte: now}, + }); + + expect(expiredoAuthLink.length).toBe(1); + expect(expiredoAuthLink.every(s => s.expiresAt <= now)).toBe(true); + }); + }); + + describe('OAuthLink deletion', () => { + it('should delete links by ID', async () => { + const oAuthLink = await OAuthLink.create({userId: testUserId}); + + await OAuthLink.findByIdAndDelete(oAuthLink._id); + + const deletedOAuthLink = await OAuthLink.findById(oAuthLink._id); + expect(deletedOAuthLink).toBeNull(); + }); + + it('should delete all links for a user', async () => { + await OAuthLink.create([{userId: testUserId}, {userId: testUserId}]); + + const result = await OAuthLink.deleteMany({userId: testUserId}); + + expect(result.deletedCount).toBe(2); + + const remainingoAuthLink = await OAuthLink.find({userId: testUserId}); + expect(remainingoAuthLink.length).toBe(0); + }); + + it('should delete expired links', async () => { + const now = new Date(); + + await OAuthLink.create([ + { + userId: testUserId, + expiresAt: new Date(now.getTime() - 1000), + }, + { + userId: testUserId, + expiresAt: new Date(now.getTime() - 2000), + }, + ]); + + const result = await OAuthLink.deleteMany({ + expiresAt: {$lte: now}, + }); + + expect(result.deletedCount).toBe(2); + }); + }); + + describe('Multiple users', () => { + let user2Id; + let user3Id; + + beforeEach(async () => { + const user2 = await User.create({ + username: 'user2', + email: 'user2@example.com', + password: 'password123', + }); + const user3 = await User.create({ + username: 'user3', + email: 'user3@example.com', + password: 'password123', + }); + user2Id = user2._id; + user3Id = user3._id; + + await OAuthLink.create([ + {userId: testUserId}, + {userId: testUserId}, + {userId: user2Id}, + {userId: user3Id}, + ]); + }); + + it('should isolate links by user', async () => { + const user1oAuthLink = await OAuthLink.find({userId: testUserId}); + const user2oAuthLink = await OAuthLink.find({userId: user2Id}); + const user3oAuthLink = await OAuthLink.find({userId: user3Id}); + + expect(user1oAuthLink.length).toBe(2); + expect(user2oAuthLink.length).toBe(1); + expect(user3oAuthLink.length).toBe(1); + }); + + it('should delete only target user links', async () => { + await OAuthLink.deleteMany({userId: testUserId}); + + const remainingUser1 = await OAuthLink.find({userId: testUserId}); + const remainingUser2 = await OAuthLink.find({userId: user2Id}); + const remainingUser3 = await OAuthLink.find({userId: user3Id}); + + expect(remainingUser1.length).toBe(0); + expect(remainingUser2.length).toBe(1); + expect(remainingUser3.length).toBe(1); + }); + + it('should count links across all users', async () => { + const totaloAuthLink = await OAuthLink.countDocuments(); + + expect(totaloAuthLink).toBe(4); + }); + }); + + describe('Others', () => { + it('should handle concurrent link creation', async () => { + const promises = Array.from({length: 10}, () => OAuthLink.create({userId: testUserId})); + + const oAuthLink = await Promise.all(promises); + + expect(oAuthLink.length).toBe(10); + expect(new Set(oAuthLink.map(s => s._id.toString())).size).toBe(10); + }); + }); +}); diff --git a/user-service/src/test/models/session 2.test.ts b/user-service/src/test/models/session 2.test.ts new file mode 100644 index 0000000000..67844a3fec --- /dev/null +++ b/user-service/src/test/models/session 2.test.ts @@ -0,0 +1,221 @@ +import mongoose from 'mongoose'; +import {REFRESH_TOKEN_DAYS} from '../../constants/expirables'; +import Session from '../../models/session'; +import User from '../../models/user'; + +describe('models/session', () => { + let testUserId; + + beforeEach(async () => { + const user = await User.create({ + username: 'testuser', + email: 'test@example.com', + password: 'password123', + }); + testUserId = user._id; + }); + + describe('Session creation', () => { + it('should create a session with required fields', async () => { + const session = await Session.create({ + userId: testUserId, + }); + + expect(session.userId).toEqual(testUserId); + expect(session.createdAt).toBeDefined(); + expect(session.createdAt).toBeInstanceOf(Date); + expect(session.expiresAt).toBeDefined(); + expect(session.expiresAt).toBeInstanceOf(Date); + }); + + it('should set default createdAt to current time', async () => { + const beforeCreate = Date.now(); + const session = await Session.create({ + userId: testUserId, + }); + const afterCreate = Date.now(); + + const createdTime = session.createdAt.getTime(); + expect(createdTime).toBeGreaterThanOrEqual(beforeCreate); + expect(createdTime).toBeLessThanOrEqual(afterCreate); + }); + + it('should set default expiresAt to REFRESH_TOKEN_DAYS in future', async () => { + const beforeCreate = Date.now(); + const session = await Session.create({ + userId: testUserId, + }); + const afterCreate = Date.now(); + + const expectedMinExpiry = beforeCreate + REFRESH_TOKEN_DAYS * 24 * 60 * 60 * 1000; + const expectedMaxExpiry = afterCreate + REFRESH_TOKEN_DAYS * 24 * 60 * 60 * 1000; + const actualExpiry = session.expiresAt.getTime(); + + expect(actualExpiry).toBeGreaterThanOrEqual(expectedMinExpiry); + expect(actualExpiry).toBeLessThanOrEqual(expectedMaxExpiry); + }); + + it('should allow custom expiresAt', async () => { + const customExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + + const session = await Session.create({ + userId: testUserId, + expiresAt: customExpiry, + }); + + expect(session.expiresAt.getTime()).toBe(customExpiry.getTime()); + }); + }); + + describe('Session validation', () => { + it('should require userId', async () => { + const sessionData = {}; + + await expect(Session.create(sessionData)).rejects.toThrow(); + }); + }); + + describe('Session queries', () => { + let createdSession; + + beforeEach(async () => { + createdSession = await Session.create({userId: testUserId}); + }); + + it('should find session by Id', async () => { + const foundSession = await Session.findById(createdSession._id); + + expect(foundSession._id.toString()).toBe(createdSession._id.toString()); + }); + + it('should find active sessions (not expired)', async () => { + const now = new Date(); + const activeSessions = await Session.find({ + userId: testUserId, + expiresAt: {$gt: now}, + }); + + expect(activeSessions.length).toBe(1); + expect(activeSessions.every(s => s.expiresAt > now)).toBe(true); + }); + + it('should find expired sessions', async () => { + const now = new Date(); + + createdSession.expiresAt = new Date(Date.now() - 1000); + await createdSession.save(); + + const expiredSessions = await Session.find({ + userId: testUserId, + expiresAt: {$lte: now}, + }); + + expect(expiredSessions.length).toBe(1); + expect(expiredSessions.every(s => s.expiresAt <= now)).toBe(true); + }); + }); + + describe('Session deletion', () => { + it('should delete session by ID', async () => { + const session = await Session.create({userId: testUserId}); + + await Session.findByIdAndDelete(session._id); + + const deletedSession = await Session.findById(session._id); + expect(deletedSession).toBeNull(); + }); + + it('should delete all sessions for a user', async () => { + await Session.create({userId: testUserId}); + + const result = await Session.deleteMany({userId: testUserId}); + + expect(result.deletedCount).toBe(1); + + const remainingSessions = await Session.find({userId: testUserId}); + expect(remainingSessions.length).toBe(0); + }); + + it('should delete expired sessions', async () => { + const now = new Date(); + + await Session.create([ + { + userId: testUserId, + expiresAt: new Date(now.getTime() - 2000), + }, + ]); + + const result = await Session.deleteMany({ + expiresAt: {$lte: now}, + }); + + expect(result.deletedCount).toBe(1); + }); + }); + + describe('Multiple users', () => { + let user2Id; + let user3Id; + + beforeEach(async () => { + const user2 = await User.create({ + username: 'user2', + email: 'user2@example.com', + password: 'password123', + }); + const user3 = await User.create({ + username: 'user3', + email: 'user3@example.com', + password: 'password123', + }); + user2Id = user2._id; + user3Id = user3._id; + + await Session.create([{userId: testUserId}, {userId: user2Id}, {userId: user3Id}]); + }); + + it('should isolate sessions by user', async () => { + const user1Sessions = await Session.find({userId: testUserId}); + const user2Sessions = await Session.find({userId: user2Id}); + const user3Sessions = await Session.find({userId: user3Id}); + + expect(user1Sessions.length).toBe(1); + expect(user2Sessions.length).toBe(1); + expect(user3Sessions.length).toBe(1); + }); + + it('should delete only target user sessions', async () => { + await Session.deleteMany({userId: testUserId}); + + const remainingUser1 = await Session.find({userId: testUserId}); + const remainingUser2 = await Session.find({userId: user2Id}); + const remainingUser3 = await Session.find({userId: user3Id}); + + expect(remainingUser1.length).toBe(0); + expect(remainingUser2.length).toBe(1); + expect(remainingUser3.length).toBe(1); + }); + + it('should count sessions across all users', async () => { + const totalSessions = await Session.countDocuments(); + + expect(totalSessions).toBe(3); + }); + }); + + describe('Others', () => { + it('should handle concurrent session creation', async () => { + const promises = Array.from({length: 10}, (_, i) => + Session.create({ + userId: new mongoose.Types.ObjectId(), + }) + ); + + const sessions = await Promise.all(promises); + + expect(sessions.length).toBe(10); + expect(new Set(sessions.map(s => s._id.toString())).size).toBe(10); + }); + }); +}); diff --git a/user-service/src/test/models/user 2.test.ts b/user-service/src/test/models/user 2.test.ts new file mode 100644 index 0000000000..4467c458cf --- /dev/null +++ b/user-service/src/test/models/user 2.test.ts @@ -0,0 +1,737 @@ +import UserRoleTypes from '../../constants/userRoles'; +import User from '../../models/user'; + +describe('models/User', () => { + const TEST_USERNAME = 'testuser'; + const TEST_USERNAME_CONFLICT = 'tEStUSeR'; + const TEST_USERNAME_DIFFERENT = 'differentuser'; + const TEST_USERNAME_ADMIN = 'admin'; + + const TEST_EMAIL = 'test@example.com'; + const TEST_EMAIL_CONFLICT = 'tESt@eXaMPlE.cOm'; + const TEST_EMAIL_DIFFERENT = 'test_different@example.com'; + const TEST_EMAIL_ADMIN = 'test_admin@admin.com'; + + const TEST_PASSWORD = 'testPassword!@#$%^'; + const TEST_PASSWORD_DIFFERENT = 'differentPassword&^%$#@'; + + const USER = UserRoleTypes.User; + const ADMIN = UserRoleTypes.Admin; + + const TEST_FIRSTNAME = 'John'; + const TEST_LASTNAME = 'Doe'; + const TEST_OCCUPATION = 'information-technology'; + const TEST_AREAOFSTUDY = 'computer-science'; + + const TEST_PROF_PIC_LINK = 'https://example.com/pic.jpg'; + const TEST_PROF_PIC_BASE64 = 'data:image/jpeg;base64, /9j/2woRAYgewoP/9k='; + + const TEST_OAUTHID = '1234567542146357'; + + describe('User creation', () => { + it('should create user with required fields', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const user = await User.create(userData); + + expect(user.username).toBe(userData.username); + expect(user.email).toBe(userData.email); + expect(user.verified).toBe(false); + expect(user.role).toBe(USER); + expect(user.profileComplete).toBe(false); + expect(user.markedForDeletion).toBe(false); + + expect(user.firstName).toBeUndefined(); + expect(user.lastName).toBeUndefined(); + expect(user.occupation).toBeUndefined(); + expect(user.areaOfStudy).toBeUndefined(); + + expect(user.googleOAuthId).toBeUndefined(); + expect(user.googleOAuthEmail).toBeUndefined(); + expect(user.googleOAuthVerified).toBeUndefined(); + + expect(user.githubOAuthId).toBeUndefined(); + expect(user.githubOAuthEmail).toBeUndefined(); + expect(user.githubOAuthVerified).toBeUndefined(); + + expect(user.profilePicture).toBeUndefined(); + expect(user.profilePictureSource).toBeUndefined(); + + expect(user.deletionScheduleAt).toBeUndefined(); + }); + + it('should create a user without password', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + }; + + const user = await User.create(userData); + + expect(user.username).toBe(userData.username); + expect(user.passwordHash).toBeUndefined(); + expect(user.passwordSalt).toBeUndefined(); + expect(user.passwordIterations).toBeUndefined(); + }); + + it('should create a user without email', async () => { + const userData = { + username: TEST_USERNAME, + password: TEST_PASSWORD, + }; + + const user = await User.create(userData); + + expect(user.username).toBe(userData.username); + expect(user.email).toBeUndefined(); + }); + + it('should hash password on creation', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const user = await User.create(userData); + + expect(user).toHaveProperty('passwordHash'); + expect(user).toHaveProperty('passwordSalt'); + expect(user).toHaveProperty('passwordIterations'); + expect(user.hasPassword).toBe(true); + }); + + it('should set timestamps on creation', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const user = await User.create(userData); + + expect(user).toHaveProperty('createdAt'); + expect(user.createdAt).toBeInstanceOf(Date); + expect(user).toHaveProperty('updatedAt'); + expect(user.updatedAt).toBeInstanceOf(Date); + }); + + it('should create user with admin role', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + role: ADMIN, + }; + + const user = await User.create(userData); + + expect(user.role).toBe(ADMIN); + }); + + it('should create verified user', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + verified: true, + }; + + const user = await User.create(userData); + + expect(user.verified).toBe(true); + }); + }); + + describe('Username validation', () => { + it('should require username', async () => { + const userData = { + password: TEST_PASSWORD, + }; + + await expect(User.create(userData)).rejects.toThrow(); + }); + + it('should enforce unique username (case-insensitive)', async () => { + const userData1 = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const userData2 = { + username: TEST_USERNAME_CONFLICT, + email: TEST_EMAIL_DIFFERENT, + password: TEST_PASSWORD, + }; + + await User.create(userData1); + await expect(User.create(userData2)).rejects.toThrow(); + }); + + it('should allow different usernames', async () => { + const userData1 = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const userData2 = { + username: TEST_USERNAME_DIFFERENT, + email: TEST_EMAIL_DIFFERENT, + password: TEST_PASSWORD, + }; + + const user1 = await User.create(userData1); + const user2 = await User.create(userData2); + + expect(user1.username).toBe(TEST_USERNAME); + expect(user2.username).toBe(TEST_USERNAME_DIFFERENT); + }); + }); + + describe('Email validation', () => { + it('should enforce unique email (case-insensitive)', async () => { + const userData1 = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const userData2 = { + username: TEST_USERNAME_DIFFERENT, + email: TEST_EMAIL_CONFLICT, + password: TEST_PASSWORD, + }; + + await User.create(userData1); + await expect(User.create(userData2)).rejects.toThrow(); + }); + + it('should allow same email for undefined/null values', async () => { + const userData1 = { + username: TEST_USERNAME, + password: TEST_PASSWORD, + }; + + const userData2 = { + username: TEST_USERNAME_DIFFERENT, + password: TEST_PASSWORD, + }; + + const user1 = await User.create(userData1); + const user2 = await User.create(userData2); + + expect(user1.email).toBeUndefined(); + expect(user2.email).toBeUndefined(); + }); + + it('should allow different emails', async () => { + const userData1 = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }; + + const userData2 = { + username: TEST_USERNAME_DIFFERENT, + email: TEST_EMAIL_DIFFERENT, + password: TEST_PASSWORD, + }; + + const user1 = await User.create(userData1); + const user2 = await User.create(userData2); + + expect(user1.email).toBe(TEST_EMAIL); + expect(user2.email).toBe(TEST_EMAIL_DIFFERENT); + }); + }); + + describe('Password validation', () => { + describe('setPassword', () => { + it('should generate different hashes for different passwords', async () => { + const user = new User({ + username: TEST_USERNAME, + email: TEST_EMAIL, + }); + + await user.setPassword(TEST_PASSWORD); + const hash1 = user.passwordHash; + + await user.setPassword(TEST_PASSWORD_DIFFERENT); + const hash2 = user.passwordHash; + + expect(hash1).not.toBe(hash2); + }); + + it('should update hasPassword flag', async () => { + const user = new User({ + username: TEST_USERNAME, + email: TEST_EMAIL, + }); + + expect(user.hasPassword).toBe(false); + + await user.setPassword(TEST_PASSWORD); + + expect(user.hasPassword).toBe(true); + }); + }); + + describe('comparePassword', () => { + it('should return true for correct password', async () => { + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }); + + const isValid = await user.comparePassword(TEST_PASSWORD); + expect(isValid).toBe(true); + }); + + it('should return false for incorrect password', async () => { + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }); + + const isValid = await user.comparePassword(TEST_PASSWORD_DIFFERENT); + expect(isValid).toBe(false); + }); + + it('should be case-sensitive', async () => { + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }); + + const isValid = await user.comparePassword(TEST_PASSWORD.toUpperCase()); + expect(isValid).toBe(false); + }); + }); + + describe('Password virtual field', () => { + it('should accept password through virtual field', async () => { + const user = new User({ + username: TEST_USERNAME, + email: TEST_EMAIL, + }); + + (user as any).password = TEST_PASSWORD; + await user.validate(); + + expect(user).toHaveProperty('passwordHash'); + expect(user.hasPassword).toBe(true); + }); + + it('should retrieve password through virtual field', () => { + const user = new User({ + username: TEST_USERNAME, + email: TEST_EMAIL, + }); + + (user as any).password = TEST_PASSWORD; + const retrievedPassword = (user as any).password; + + expect(retrievedPassword).toBe(TEST_PASSWORD); + }); + }); + }); + + describe('Personal ation fields', () => { + it('should store personal information provided', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + firstName: TEST_FIRSTNAME, + lastName: TEST_LASTNAME, + occupation: TEST_OCCUPATION, + areaOfStudy: TEST_AREAOFSTUDY, + profileComplete: true, + }; + + const user = await User.create(userData); + + expect(user.firstName).toBe(TEST_FIRSTNAME); + expect(user.lastName).toBe(TEST_LASTNAME); + expect(user.occupation).toBe(TEST_OCCUPATION); + expect(user.areaOfStudy).toBe(TEST_AREAOFSTUDY); + expect(user.profileComplete).toBe(true); + }); + }); + + describe('OAuth fields', () => { + describe('Google OAuth', () => { + it('should store Google OAuth information', async () => { + const userData = { + username: TEST_USERNAME, + googleOAuthId: TEST_OAUTHID, + googleOAuthEmail: TEST_EMAIL, + googleOAuthVerified: true, + }; + + const user = await User.create(userData); + + expect(user.googleOAuthId).toBe(TEST_OAUTHID); + expect(user.googleOAuthEmail).toBe(TEST_EMAIL); + expect(user.googleOAuthVerified).toBe(true); + }); + }); + + describe('GitHub OAuth', () => { + it('should store GitHub OAuth information', async () => { + const userData = { + username: TEST_USERNAME, + githubOAuthId: TEST_OAUTHID, + githubOAuthEmail: TEST_EMAIL, + githubOAuthVerified: true, + }; + + const user = await User.create(userData); + + expect(user.githubOAuthId).toBe(TEST_OAUTHID); + expect(user.githubOAuthEmail).toBe(TEST_EMAIL); + expect(user.githubOAuthVerified).toBe(true); + }); + }); + + it('should allow both OAuth providers', async () => { + const userData = { + username: TEST_USERNAME, + googleOAuthId: TEST_OAUTHID, + googleOAuthEmail: TEST_EMAIL, + githubOAuthId: TEST_OAUTHID, + githubOAuthEmail: TEST_EMAIL, + }; + + const user = await User.create(userData); + + expect(user.googleOAuthId).toBe(TEST_OAUTHID); + expect(user.githubOAuthId).toBe(TEST_OAUTHID); + }); + }); + + describe('Profile picture', () => { + it('should store profile picture URL', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + profilePicture: TEST_PROF_PIC_LINK, + profilePictureSource: 'google', + }; + + const user = await User.create(userData); + + expect(user.profilePicture).toBe(TEST_PROF_PIC_LINK); + expect(user.profilePictureSource).toBe('google'); + }); + + it('should store base64 encoded image', async () => { + const base64Image = TEST_PROF_PIC_BASE64; + + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + profilePicture: base64Image, + profilePictureSource: 'upload', + }; + + const user = await User.create(userData); + + expect(user.profilePicture).toBe(base64Image); + expect(user.profilePictureSource).toBe('upload'); + }); + }); + + describe('Account deletion fields', () => { + it('should mark account for deletion', async () => { + const deletionDate = new Date(Date.now()); + + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + markedForDeletion: true, + deletionScheduleAt: deletionDate, + }; + + const user = await User.create(userData); + + expect(user.markedForDeletion).toBe(true); + expect(user.deletionScheduleAt).toEqual(deletionDate); + }); + + it('should allow unmarking for deletion', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + markedForDeletion: true, + deletionScheduleAt: new Date(Date.now()), + }; + + const user = await User.create(userData); + expect(user.markedForDeletion).toBe(true); + + user.markedForDeletion = false; + user.deletionScheduleAt = undefined; + await user.save(); + + expect(user.markedForDeletion).toBe(false); + expect(user).toHaveProperty('deletionScheduleAt'); + expect(user.deletionScheduleAt).toBeUndefined(); + }); + }); + + describe('User queries', () => { + const EXPECT_VALID = 2; + const EXPECT_ADMIN = 1; + + beforeEach(async () => { + await User.create([ + { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + verified: true, + }, + { + username: TEST_USERNAME_DIFFERENT, + email: TEST_EMAIL_DIFFERENT, + password: TEST_PASSWORD, + verified: false, + }, + { + username: TEST_USERNAME_ADMIN, + email: TEST_EMAIL_ADMIN, + password: TEST_PASSWORD, + verified: true, + role: ADMIN, + }, + ]); + }); + + it('should find user by username', async () => { + const user = await User.findOne({username: TEST_USERNAME}); + + expect(user).not.toBeNull(); + expect(user.username).toBe(TEST_USERNAME); + }); + + it('should find user by email', async () => { + const user = await User.findOne({email: TEST_EMAIL}); + + expect(user).not.toBeNull(); + expect(user.email).toBe(TEST_EMAIL); + }); + + it('should find user by ID', async () => { + const createdUser = await User.findOne({username: TEST_USERNAME}); + const foundUser = await User.findById(createdUser._id); + + expect(foundUser).not.toBeNull(); + expect(foundUser._id.toString()).toBe(createdUser._id.toString()); + }); + + it('should find verified users', async () => { + const verifiedUsers = await User.find({verified: true}); + + expect(verifiedUsers.length).toBe(EXPECT_VALID); + expect(verifiedUsers.every(u => u.verified)).toBe(true); + }); + + it('should find users by role', async () => { + const admins = await User.find({role: ADMIN}); + + expect(admins.length).toBe(1); + expect(admins.every(u => u.role === ADMIN)).toBe(true); + }); + + it('should support case-insensitive username search', async () => { + const user = await User.findOne({username: TEST_USERNAME_CONFLICT}).collation({ + locale: 'en', + strength: 2, + }); + + expect(user).not.toBeNull(); + expect(user.username).toBe(TEST_USERNAME); + }); + + it('should support case-insensitive email search', async () => { + const user = await User.findOne({email: TEST_EMAIL_CONFLICT}).collation({ + locale: 'en', + strength: 2, + }); + + expect(user).not.toBeNull(); + expect(user.email).toBe(TEST_EMAIL); + }); + }); + + describe('User updates', () => { + it('should update updatedAt timestamp', async () => { + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }); + + const originalUpdatedAt = user.updatedAt; + + user.username = TEST_USERNAME_DIFFERENT; + user.firstName = TEST_FIRSTNAME; + await user.save(); + + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(user.username).toBe(TEST_USERNAME_DIFFERENT); + expect(user.firstName).toBe(TEST_FIRSTNAME); + expect(user.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime()); + }); + + it('should update password correctly', async () => { + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }); + + const oldHash = user.passwordHash; + + (user as any).password = TEST_PASSWORD_DIFFERENT; + await user.save(); + + expect(user.passwordHash).not.toBe(oldHash); + + const isOldValid = await user.comparePassword(TEST_PASSWORD); + const isNewValid = await user.comparePassword(TEST_PASSWORD_DIFFERENT); + + expect(isOldValid).toBe(false); + expect(isNewValid).toBe(true); + }); + }); + + describe('Password rehashing', () => { + it('should warn when password needs rehashing but password not available', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + }); + + user.passwordIterations = 1000; + await user.save(); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Password needs rehashing but password not available' + ); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('toJSON method', () => { + it('should exclude irrelevant fields from JSON', async () => { + const user = await User.create({ + username: TEST_USERNAME, + email: TEST_EMAIL, + verified: true, + password: TEST_PASSWORD, + firstName: TEST_FIRSTNAME, + lastName: TEST_LASTNAME, + occupation: TEST_OCCUPATION, + areaOfStudy: TEST_AREAOFSTUDY, + profileComplete: true, + googleOAuthId: TEST_OAUTHID, + googleOAuthEmail: TEST_EMAIL, + googleOAuthVerified: true, + githubOAuthId: TEST_OAUTHID, + githubOAuthEmail: TEST_EMAIL, + githubOAuthVerified: true, + profilePicture: TEST_PROF_PIC_BASE64, + profilePictureSource: 'upload', + markedForDeletion: true, + deletionScheduleAt: new Date(Date.now()), + }); + + const json = user.toJSON(); + + expect(json.username).toBe(TEST_USERNAME); + expect(json.email).toBe(TEST_EMAIL); + expect(json.verified).toBe(true); + expect(json.firstName).toBe(TEST_FIRSTNAME); + expect(json.lastName).toBe(TEST_LASTNAME); + expect(json.occupation).toBe(TEST_OCCUPATION); + expect(json.areaOfStudy).toBe(TEST_AREAOFSTUDY); + expect(json.profileComplete).toBe(true); + expect(json.googleOAuthId).not.toBe(TEST_OAUTHID); + expect(json.googleOAuthEmail).toBe(TEST_EMAIL); + expect(json.googleOAuthVerified).toBe(true); + expect(json.githubOAuthId).not.toBe(TEST_OAUTHID); + expect(json.githubOAuthEmail).toBe(TEST_EMAIL); + expect(json.githubOAuthVerified).toBe(true); + expect(json.profilePicture).toBe(TEST_PROF_PIC_BASE64); + expect(json.profilePictureSource).toBe('upload'); + + expect(json).not.toHaveProperty('passwordHash'); + expect(json).not.toHaveProperty('passwordSalt'); + expect(json).not.toHaveProperty('passwordIterations'); + expect(json).not.toHaveProperty('markedForDeletion'); + expect(json).not.toHaveProperty('deletionScheduleAt'); + }); + }); + + describe('Others', () => { + it('should handle very long username', async () => { + const longUsername = 'a'.repeat(30); + + const user = await User.create({ + username: longUsername, + email: TEST_USERNAME, + password: TEST_PASSWORD, + }); + + expect(user.username).toBe(longUsername); + }); + + it('should handle unicode characters in names', async () => { + const userData = { + username: TEST_USERNAME, + email: TEST_EMAIL, + password: TEST_PASSWORD, + firstName: "O'Brien", + lastName: 'José-María', + }; + + const user = await User.create(userData); + + expect(user.firstName).toBe("O'Brien"); + expect(user.lastName).toBe('José-María'); + }); + + it('should handle concurrent user creation', async () => { + const users = Array.from({length: 10}, (_, i) => ({ + username: `user${i}`, + email: `user${i}@example.com`, + password: TEST_PASSWORD, + })); + + const createdUsers = await Promise.all(users.map(userData => User.create(userData))); + + expect(createdUsers).toHaveLength(10); + expect(new Set(createdUsers.map(u => u.username)).size).toBe(10); + }); + }); +}); diff --git a/user-service/src/test/models/verificationCode 2.test.ts b/user-service/src/test/models/verificationCode 2.test.ts new file mode 100644 index 0000000000..5ac55e165d --- /dev/null +++ b/user-service/src/test/models/verificationCode 2.test.ts @@ -0,0 +1,431 @@ +import VerificationType from '../../constants/verificationTypes'; +import User from '../../models/user'; +import VerificationCode from '../../models/verificationCode'; + +describe('models/verificationCode', () => { + const TEST_EXPIRY_BEFORE = new Date(Date.now() - 1000000); + const TEST_EXPIRY = new Date(Date.now() + 1000000); + const TEST_EXPIRY_AFTER = new Date(Date.now() + 2000000); + let testUserId; + + beforeEach(async () => { + const user = await User.create({ + username: 'testuser', + email: 'test@example.com', + password: 'password123', + }); + testUserId = user._id; + }); + + describe('VerificationCode creation', () => { + it('should create a verification code with required fields', async () => { + const verificationCode = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + expect(verificationCode.userId).toEqual(testUserId); + expect(verificationCode.type).toBe(VerificationType.VerifyEmail); + expect(verificationCode.expiresAt).toEqual(TEST_EXPIRY); + expect(verificationCode).toHaveProperty('createdAt'); + expect(verificationCode.createdAt).toBeInstanceOf(Date); + }); + + it('should set default createdAt to current time', async () => { + const beforeCreate = Date.now(); + + const verificationCode = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + const afterCreate = Date.now(); + + const createdTime = verificationCode.createdAt.getTime(); + expect(createdTime).toBeGreaterThanOrEqual(beforeCreate); + expect(createdTime).toBeLessThanOrEqual(afterCreate); + }); + + it('should generate unique IDs for verification codes', async () => { + const code1 = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + const code2 = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + expect(code1._id.toString()).not.toBe(code2._id.toString()); + }); + }); + + describe('Verification types', () => { + it('should create VerifyEmail type', async () => { + const code = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + expect(code.type).toBe(VerificationType.VerifyEmail); + }); + + it('should create ResetPassword type', async () => { + const code = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.ResetPassword, + expiresAt: TEST_EXPIRY, + }); + + expect(code.type).toBe(VerificationType.ResetPassword); + }); + + it('should allow different types for same user', async () => { + const emailCode = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + const passwordCode = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.ResetPassword, + expiresAt: TEST_EXPIRY, + }); + + expect(emailCode.type).toBe(VerificationType.VerifyEmail); + expect(passwordCode.type).toBe(VerificationType.ResetPassword); + }); + }); + + describe('Verification validation', () => { + it('should require userId', async () => { + const codeData = { + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }; + + await expect(VerificationCode.create(codeData)).rejects.toThrow(); + }); + + it('should require type', async () => { + const codeData = { + userId: testUserId, + expiresAt: TEST_EXPIRY, + }; + + await expect(VerificationCode.create(codeData)).rejects.toThrow(); + }); + + it('should require expiresAt', async () => { + const codeData = { + userId: testUserId, + type: VerificationType.VerifyEmail, + }; + + await expect(VerificationCode.create(codeData)).rejects.toThrow(); + }); + + it('should require valid ObjectId for userId', async () => { + const codeData = { + userId: 'invalid-id', + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }; + + await expect(VerificationCode.create(codeData)).rejects.toThrow(); + }); + + it('should require date for expiresAt', async () => { + const codeData = { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: 'not-a-date', + }; + + await expect(VerificationCode.create(codeData)).rejects.toThrow(); + }); + + it('should require date for createdAt', async () => { + const codeData = { + userId: testUserId, + type: VerificationType.VerifyEmail, + createdAt: 'not-a-date', + expiresAt: new Date(), + }; + + await expect(VerificationCode.create(codeData)).rejects.toThrow(); + }); + }); + + describe('User reference', () => { + it('should create multiple codes for same user', async () => { + const code1 = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + const code2 = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + expect(code1.userId).toEqual(testUserId); + expect(code2.userId).toEqual(testUserId); + expect(code1._id.toString()).not.toBe(code2._id.toString()); + }); + }); + + describe('VerificationCode queries', () => { + beforeEach(async () => { + const now = Date.now(); + + await VerificationCode.create([ + { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }, + { + userId: testUserId, + type: VerificationType.ResetPassword, + expiresAt: TEST_EXPIRY_AFTER, + }, + { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY_BEFORE, // Expired + }, + ]); + }); + + it('should find all codes for a user', async () => { + const codes = await VerificationCode.find({userId: testUserId}); + + expect(codes.length).toBe(3); + expect(codes.every(c => c.userId.equals(testUserId))).toBe(true); + }); + + it('should find code by Id', async () => { + const createdCode = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + const foundCode = await VerificationCode.findById(createdCode._id); + + expect(foundCode).not.toBeNull(); + expect(foundCode._id.toString()).toBe(createdCode._id.toString()); + }); + + it('should find codes by type', async () => { + const emailCodes = await VerificationCode.find({ + userId: testUserId, + type: VerificationType.VerifyEmail, + }); + + expect(emailCodes.length).toBe(2); + expect(emailCodes.every(c => c.type === VerificationType.VerifyEmail)).toBe(true); + }); + + it('should find valid codes (non-expired)', async () => { + const now = new Date(); + const validCodes = await VerificationCode.find({ + userId: testUserId, + expiresAt: {$gt: now}, + }); + + expect(validCodes.length).toBe(2); + expect(validCodes.every(c => c.expiresAt > now)).toBe(true); + }); + + it('should find expired codes', async () => { + const now = new Date(); + const expiredCodes = await VerificationCode.find({ + userId: testUserId, + expiresAt: {$lte: now}, + }); + + expect(expiredCodes.length).toBe(1); + expect(expiredCodes.every(c => c.expiresAt <= now)).toBe(true); + }); + }); + + describe('VerificationCode deletion', () => { + it('should delete code by ID', async () => { + const code = await VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }); + + await VerificationCode.findByIdAndDelete(code._id); + + const deletedCode = await VerificationCode.findById(code._id); + expect(deletedCode).toBeNull(); + }); + + it('should delete all codes for a user', async () => { + await VerificationCode.create([ + { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }, + { + userId: testUserId, + type: VerificationType.ResetPassword, + expiresAt: TEST_EXPIRY, + }, + ]); + + const result = await VerificationCode.deleteMany({userId: testUserId}); + + expect(result.deletedCount).toBe(2); + + const remainingCodes = await VerificationCode.find({userId: testUserId}); + expect(remainingCodes.length).toBe(0); + }); + + it('should delete expired codes', async () => { + const now = new Date(); + + await VerificationCode.create([ + { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY_BEFORE, + }, + { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: new Date(now), + }, + ]); + + const result = await VerificationCode.deleteMany({ + expiresAt: {$lte: now}, + }); + + expect(result.deletedCount).toBe(2); + }); + + it('should delete codes by type', async () => { + await VerificationCode.create([ + { + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }, + { + userId: testUserId, + type: VerificationType.ResetPassword, + expiresAt: TEST_EXPIRY, + }, + ]); + + const result = await VerificationCode.deleteMany({ + userId: testUserId, + type: VerificationType.VerifyEmail, + }); + + expect(result.deletedCount).toBe(1); + + const remainingCodes = await VerificationCode.find({userId: testUserId}); + expect(remainingCodes.every(c => c.type === VerificationType.ResetPassword)).toBe(true); + }); + }); + + describe('Multiple users', () => { + let user2Id; + let user3Id; + + beforeEach(async () => { + const user2 = await User.create({ + username: 'user2', + email: 'user2@example.com', + password: 'password123', + }); + const user3 = await User.create({ + username: 'user3', + email: 'user3@example.com', + password: 'password123', + }); + user2Id = user2._id; + user3Id = user3._id; + + await VerificationCode.create([ + {userId: testUserId, type: VerificationType.VerifyEmail, expiresAt: TEST_EXPIRY}, + {userId: testUserId, type: VerificationType.ResetPassword, expiresAt: TEST_EXPIRY}, + {userId: user2Id, type: VerificationType.VerifyEmail, expiresAt: TEST_EXPIRY}, + {userId: user3Id, type: VerificationType.ResetPassword, expiresAt: TEST_EXPIRY}, + ]); + }); + + it('should isolate codes by user', async () => { + const user1Codes = await VerificationCode.find({userId: testUserId}); + const user2Codes = await VerificationCode.find({userId: user2Id}); + const user3Codes = await VerificationCode.find({userId: user3Id}); + + expect(user1Codes.length).toBe(2); + expect(user2Codes.length).toBe(1); + expect(user3Codes.length).toBe(1); + }); + + it('should delete only target user codes', async () => { + await VerificationCode.deleteMany({userId: testUserId}); + + const remainingUser1 = await VerificationCode.find({userId: testUserId}); + const remainingUser2 = await VerificationCode.find({userId: user2Id}); + const remainingUser3 = await VerificationCode.find({userId: user3Id}); + + expect(remainingUser1.length).toBe(0); + expect(remainingUser2.length).toBe(1); + expect(remainingUser3.length).toBe(1); + }); + + it('should count codes across all users', async () => { + const totalCodes = await VerificationCode.countDocuments(); + + expect(totalCodes).toBe(4); + }); + + it('should find codes by type across users', async () => { + const emailCodes = await VerificationCode.find({ + type: VerificationType.VerifyEmail, + }); + const passwordCodes = await VerificationCode.find({ + type: VerificationType.ResetPassword, + }); + + expect(emailCodes.length).toBe(2); + expect(passwordCodes.length).toBe(2); + }); + }); + + describe('Others', () => { + it('should handle concurrent code creation', async () => { + const promises = Array.from({length: 10}, () => + VerificationCode.create({ + userId: testUserId, + type: VerificationType.VerifyEmail, + expiresAt: TEST_EXPIRY, + }) + ); + + const codes = await Promise.all(promises); + + expect(codes.length).toBe(10); + expect(new Set(codes.map(c => c._id.toString())).size).toBe(10); + }); + }); +}); From 41f2a3c0efaea7c95d583d4628021cb4c9685c19 Mon Sep 17 00:00:00 2001 From: Vera Date: Thu, 13 Nov 2025 04:57:20 +0800 Subject: [PATCH 2/7] Delete user-service/src/test/models/oAuthLink 2.test.ts --- .../src/test/models/oAuthLink 2.test.ts | 242 ------------------ 1 file changed, 242 deletions(-) delete mode 100644 user-service/src/test/models/oAuthLink 2.test.ts diff --git a/user-service/src/test/models/oAuthLink 2.test.ts b/user-service/src/test/models/oAuthLink 2.test.ts deleted file mode 100644 index 2c42ab95af..0000000000 --- a/user-service/src/test/models/oAuthLink 2.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import {OAUTH_LINK_MINS} from '../../constants/expirables'; -import OAuthLink from '../../models/oAuthLink'; -import User from '../../models/user'; - -describe('models/oAuthLink', () => { - let testUserId; - - beforeEach(async () => { - const user = await User.create({ - username: 'testuser', - email: 'test@example.com', - password: 'password123', - }); - testUserId = user._id; - }); - - describe('Link creation', () => { - it('should create a link with required fields', async () => { - const oAuthLink = await OAuthLink.create({ - userId: testUserId, - }); - - expect(oAuthLink.userId).toEqual(testUserId); - expect(oAuthLink.createdAt).toBeDefined(); - expect(oAuthLink.createdAt).toBeInstanceOf(Date); - expect(oAuthLink.expiresAt).toBeDefined(); - expect(oAuthLink.expiresAt).toBeInstanceOf(Date); - }); - - it('should set default createdAt to current time', async () => { - const beforeCreate = Date.now(); - const oAuthLink = await OAuthLink.create({ - userId: testUserId, - }); - const afterCreate = Date.now(); - - const createdTime = oAuthLink.createdAt.getTime(); - expect(createdTime).toBeGreaterThanOrEqual(beforeCreate); - expect(createdTime).toBeLessThanOrEqual(afterCreate); - }); - - it('should set default expiresAt to OAUTH_LINK_MINS in future', async () => { - const beforeCreate = Date.now(); - const oAuthLink = await OAuthLink.create({ - userId: testUserId, - }); - const afterCreate = Date.now(); - - const expectedMinExpiry = beforeCreate + OAUTH_LINK_MINS * 60 * 1000; - const expectedMaxExpiry = afterCreate + OAUTH_LINK_MINS * 60 * 1000; - const actualExpiry = oAuthLink.expiresAt.getTime(); - - expect(actualExpiry).toBeGreaterThanOrEqual(expectedMinExpiry); - expect(actualExpiry).toBeLessThanOrEqual(expectedMaxExpiry); - }); - - it('should allow custom expiresAt', async () => { - const customExpiry = new Date(Date.now() + 7 * 60 * 1000); - - const oAuthLink = await OAuthLink.create({ - userId: testUserId, - expiresAt: customExpiry, - }); - - expect(oAuthLink.expiresAt.getTime()).toBe(customExpiry.getTime()); - }); - - it('should generate unique link Ids', async () => { - const oAuthLink1 = await OAuthLink.create({userId: testUserId}); - const oAuthLink2 = await OAuthLink.create({userId: testUserId}); - - expect(oAuthLink1._id.toString()).not.toBe(oAuthLink2._id.toString()); - }); - }); - - describe('OAuthLink validation', () => { - it('should require userId', async () => { - const oAuthLinkData = {}; - - await expect(OAuthLink.create(oAuthLinkData)).rejects.toThrow(); - }); - }); - - describe('OAuthLink queries', () => { - beforeEach(async () => { - await OAuthLink.create([ - {userId: testUserId}, - {userId: testUserId}, - { - userId: testUserId, - expiresAt: new Date(Date.now() - 1000), // Expired - }, - ]); - }); - - it('should find all links for a user', async () => { - const oAuthLink = await OAuthLink.find({userId: testUserId}); - - expect(oAuthLink.length).toBe(3); - expect(oAuthLink.every(s => s.userId.equals(testUserId))).toBe(true); - }); - - it('should find links by Id', async () => { - const createdOAuthLink = await OAuthLink.create({userId: testUserId}); - const foundOAuthLink = await OAuthLink.findById(createdOAuthLink._id); - - expect(foundOAuthLink).not.toBeNull(); - expect(foundOAuthLink._id.toString()).toBe(createdOAuthLink._id.toString()); - }); - - it('should find active links (not expired)', async () => { - const now = new Date(); - const activeoAuthLink = await OAuthLink.find({ - userId: testUserId, - expiresAt: {$gt: now}, - }); - - expect(activeoAuthLink.length).toBe(2); - expect(activeoAuthLink.every(s => s.expiresAt > now)).toBe(true); - }); - - it('should find expired links', async () => { - const now = new Date(); - const expiredoAuthLink = await OAuthLink.find({ - userId: testUserId, - expiresAt: {$lte: now}, - }); - - expect(expiredoAuthLink.length).toBe(1); - expect(expiredoAuthLink.every(s => s.expiresAt <= now)).toBe(true); - }); - }); - - describe('OAuthLink deletion', () => { - it('should delete links by ID', async () => { - const oAuthLink = await OAuthLink.create({userId: testUserId}); - - await OAuthLink.findByIdAndDelete(oAuthLink._id); - - const deletedOAuthLink = await OAuthLink.findById(oAuthLink._id); - expect(deletedOAuthLink).toBeNull(); - }); - - it('should delete all links for a user', async () => { - await OAuthLink.create([{userId: testUserId}, {userId: testUserId}]); - - const result = await OAuthLink.deleteMany({userId: testUserId}); - - expect(result.deletedCount).toBe(2); - - const remainingoAuthLink = await OAuthLink.find({userId: testUserId}); - expect(remainingoAuthLink.length).toBe(0); - }); - - it('should delete expired links', async () => { - const now = new Date(); - - await OAuthLink.create([ - { - userId: testUserId, - expiresAt: new Date(now.getTime() - 1000), - }, - { - userId: testUserId, - expiresAt: new Date(now.getTime() - 2000), - }, - ]); - - const result = await OAuthLink.deleteMany({ - expiresAt: {$lte: now}, - }); - - expect(result.deletedCount).toBe(2); - }); - }); - - describe('Multiple users', () => { - let user2Id; - let user3Id; - - beforeEach(async () => { - const user2 = await User.create({ - username: 'user2', - email: 'user2@example.com', - password: 'password123', - }); - const user3 = await User.create({ - username: 'user3', - email: 'user3@example.com', - password: 'password123', - }); - user2Id = user2._id; - user3Id = user3._id; - - await OAuthLink.create([ - {userId: testUserId}, - {userId: testUserId}, - {userId: user2Id}, - {userId: user3Id}, - ]); - }); - - it('should isolate links by user', async () => { - const user1oAuthLink = await OAuthLink.find({userId: testUserId}); - const user2oAuthLink = await OAuthLink.find({userId: user2Id}); - const user3oAuthLink = await OAuthLink.find({userId: user3Id}); - - expect(user1oAuthLink.length).toBe(2); - expect(user2oAuthLink.length).toBe(1); - expect(user3oAuthLink.length).toBe(1); - }); - - it('should delete only target user links', async () => { - await OAuthLink.deleteMany({userId: testUserId}); - - const remainingUser1 = await OAuthLink.find({userId: testUserId}); - const remainingUser2 = await OAuthLink.find({userId: user2Id}); - const remainingUser3 = await OAuthLink.find({userId: user3Id}); - - expect(remainingUser1.length).toBe(0); - expect(remainingUser2.length).toBe(1); - expect(remainingUser3.length).toBe(1); - }); - - it('should count links across all users', async () => { - const totaloAuthLink = await OAuthLink.countDocuments(); - - expect(totaloAuthLink).toBe(4); - }); - }); - - describe('Others', () => { - it('should handle concurrent link creation', async () => { - const promises = Array.from({length: 10}, () => OAuthLink.create({userId: testUserId})); - - const oAuthLink = await Promise.all(promises); - - expect(oAuthLink.length).toBe(10); - expect(new Set(oAuthLink.map(s => s._id.toString())).size).toBe(10); - }); - }); -}); From c633e160de2deb58be53e224c9285cb0fe12bcf0 Mon Sep 17 00:00:00 2001 From: Vera Date: Thu, 13 Nov 2025 04:57:39 +0800 Subject: [PATCH 3/7] Delete user-service/src/test/models/session 2.test.ts --- .../src/test/models/session 2.test.ts | 221 ------------------ 1 file changed, 221 deletions(-) delete mode 100644 user-service/src/test/models/session 2.test.ts diff --git a/user-service/src/test/models/session 2.test.ts b/user-service/src/test/models/session 2.test.ts deleted file mode 100644 index 67844a3fec..0000000000 --- a/user-service/src/test/models/session 2.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import mongoose from 'mongoose'; -import {REFRESH_TOKEN_DAYS} from '../../constants/expirables'; -import Session from '../../models/session'; -import User from '../../models/user'; - -describe('models/session', () => { - let testUserId; - - beforeEach(async () => { - const user = await User.create({ - username: 'testuser', - email: 'test@example.com', - password: 'password123', - }); - testUserId = user._id; - }); - - describe('Session creation', () => { - it('should create a session with required fields', async () => { - const session = await Session.create({ - userId: testUserId, - }); - - expect(session.userId).toEqual(testUserId); - expect(session.createdAt).toBeDefined(); - expect(session.createdAt).toBeInstanceOf(Date); - expect(session.expiresAt).toBeDefined(); - expect(session.expiresAt).toBeInstanceOf(Date); - }); - - it('should set default createdAt to current time', async () => { - const beforeCreate = Date.now(); - const session = await Session.create({ - userId: testUserId, - }); - const afterCreate = Date.now(); - - const createdTime = session.createdAt.getTime(); - expect(createdTime).toBeGreaterThanOrEqual(beforeCreate); - expect(createdTime).toBeLessThanOrEqual(afterCreate); - }); - - it('should set default expiresAt to REFRESH_TOKEN_DAYS in future', async () => { - const beforeCreate = Date.now(); - const session = await Session.create({ - userId: testUserId, - }); - const afterCreate = Date.now(); - - const expectedMinExpiry = beforeCreate + REFRESH_TOKEN_DAYS * 24 * 60 * 60 * 1000; - const expectedMaxExpiry = afterCreate + REFRESH_TOKEN_DAYS * 24 * 60 * 60 * 1000; - const actualExpiry = session.expiresAt.getTime(); - - expect(actualExpiry).toBeGreaterThanOrEqual(expectedMinExpiry); - expect(actualExpiry).toBeLessThanOrEqual(expectedMaxExpiry); - }); - - it('should allow custom expiresAt', async () => { - const customExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); - - const session = await Session.create({ - userId: testUserId, - expiresAt: customExpiry, - }); - - expect(session.expiresAt.getTime()).toBe(customExpiry.getTime()); - }); - }); - - describe('Session validation', () => { - it('should require userId', async () => { - const sessionData = {}; - - await expect(Session.create(sessionData)).rejects.toThrow(); - }); - }); - - describe('Session queries', () => { - let createdSession; - - beforeEach(async () => { - createdSession = await Session.create({userId: testUserId}); - }); - - it('should find session by Id', async () => { - const foundSession = await Session.findById(createdSession._id); - - expect(foundSession._id.toString()).toBe(createdSession._id.toString()); - }); - - it('should find active sessions (not expired)', async () => { - const now = new Date(); - const activeSessions = await Session.find({ - userId: testUserId, - expiresAt: {$gt: now}, - }); - - expect(activeSessions.length).toBe(1); - expect(activeSessions.every(s => s.expiresAt > now)).toBe(true); - }); - - it('should find expired sessions', async () => { - const now = new Date(); - - createdSession.expiresAt = new Date(Date.now() - 1000); - await createdSession.save(); - - const expiredSessions = await Session.find({ - userId: testUserId, - expiresAt: {$lte: now}, - }); - - expect(expiredSessions.length).toBe(1); - expect(expiredSessions.every(s => s.expiresAt <= now)).toBe(true); - }); - }); - - describe('Session deletion', () => { - it('should delete session by ID', async () => { - const session = await Session.create({userId: testUserId}); - - await Session.findByIdAndDelete(session._id); - - const deletedSession = await Session.findById(session._id); - expect(deletedSession).toBeNull(); - }); - - it('should delete all sessions for a user', async () => { - await Session.create({userId: testUserId}); - - const result = await Session.deleteMany({userId: testUserId}); - - expect(result.deletedCount).toBe(1); - - const remainingSessions = await Session.find({userId: testUserId}); - expect(remainingSessions.length).toBe(0); - }); - - it('should delete expired sessions', async () => { - const now = new Date(); - - await Session.create([ - { - userId: testUserId, - expiresAt: new Date(now.getTime() - 2000), - }, - ]); - - const result = await Session.deleteMany({ - expiresAt: {$lte: now}, - }); - - expect(result.deletedCount).toBe(1); - }); - }); - - describe('Multiple users', () => { - let user2Id; - let user3Id; - - beforeEach(async () => { - const user2 = await User.create({ - username: 'user2', - email: 'user2@example.com', - password: 'password123', - }); - const user3 = await User.create({ - username: 'user3', - email: 'user3@example.com', - password: 'password123', - }); - user2Id = user2._id; - user3Id = user3._id; - - await Session.create([{userId: testUserId}, {userId: user2Id}, {userId: user3Id}]); - }); - - it('should isolate sessions by user', async () => { - const user1Sessions = await Session.find({userId: testUserId}); - const user2Sessions = await Session.find({userId: user2Id}); - const user3Sessions = await Session.find({userId: user3Id}); - - expect(user1Sessions.length).toBe(1); - expect(user2Sessions.length).toBe(1); - expect(user3Sessions.length).toBe(1); - }); - - it('should delete only target user sessions', async () => { - await Session.deleteMany({userId: testUserId}); - - const remainingUser1 = await Session.find({userId: testUserId}); - const remainingUser2 = await Session.find({userId: user2Id}); - const remainingUser3 = await Session.find({userId: user3Id}); - - expect(remainingUser1.length).toBe(0); - expect(remainingUser2.length).toBe(1); - expect(remainingUser3.length).toBe(1); - }); - - it('should count sessions across all users', async () => { - const totalSessions = await Session.countDocuments(); - - expect(totalSessions).toBe(3); - }); - }); - - describe('Others', () => { - it('should handle concurrent session creation', async () => { - const promises = Array.from({length: 10}, (_, i) => - Session.create({ - userId: new mongoose.Types.ObjectId(), - }) - ); - - const sessions = await Promise.all(promises); - - expect(sessions.length).toBe(10); - expect(new Set(sessions.map(s => s._id.toString())).size).toBe(10); - }); - }); -}); From bfb5c9ed616077a4f31bea3d81144ac3f3ad86cf Mon Sep 17 00:00:00 2001 From: Vera Date: Thu, 13 Nov 2025 04:57:58 +0800 Subject: [PATCH 4/7] Delete frontend/styles/profile.css --- frontend/styles/profile.css | 289 ------------------------------------ 1 file changed, 289 deletions(-) delete mode 100644 frontend/styles/profile.css diff --git a/frontend/styles/profile.css b/frontend/styles/profile.css deleted file mode 100644 index 106d4b8852..0000000000 --- a/frontend/styles/profile.css +++ /dev/null @@ -1,289 +0,0 @@ -.verify-prompt-wrapper { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - min-height: 100vh; - padding: 2rem; -} - -.verify-prompt-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 2rem; - padding: 3rem 2rem; - border-radius: 0.5rem; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); - max-width: 50rem; - width: 100%; - text-align: center; -} - -.alert-warning { - background-color: #fef3c7; - border: 3px solid #fbbf24; - color: #92400e; -} - -.verify-prompt-content { - display: flex; - flex-direction: column; - text-align: center; - gap: 1rem; -} - -.verify-prompt-title { - font-size: 23px; - font-weight: 500; - color: #1f2937; - margin: 0; -} - -.verify-prompt-message { - font-size: 17px; - line-height: 1.6; - color: #6b7280; - margin: 0; -} - -.verify-prompt-actions { - display: flex; - flex-direction: column; - gap: 2rem; - width: 100%; - align-items: center; -} - -.verify-prompt-note { - font-size: 17px; - color: #6b7280; - margin: 0; -} - -.link-button { - background: none; - border: none; - color: #4285f4; - text-decoration: none; - cursor: pointer; - padding: 0; - font-size: inherit; - transition: all 0.3s ease; -} - -.link-button:hover { - text-decoration: underline; -} - -.profile-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.profile-main { - display: flex; - flex-direction: column; - flex: 1; - padding: 2rem 3rem; - gap: 2rem; -} - -.welcome-section { - display: flex; - justify-content: space-between; - align-items: center; - gap: 2rem; -} - -.welcome-content { - display: flex; - flex-direction: column; -} - -.welcome-title { - font-size: 45px; - font-weight: 800; - color: #303030; - margin-bottom: 0.5rem; -} - -.welcome-username-highlight { - color: #4181f5; -} - -.welcome-subtitle { - font-size: 23px; - font-weight: 500; - color: #627287; -} - -.stats-section { - display: flex; - flex-wrap: wrap; - gap: 2rem; -} - -.stat-card { - display: flex; - align-items: center; - justify-content: space-between; - flex: 1; - padding: 1.5rem; - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - gap: 1rem; -} - -.stat-info { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.stat-value { - font-size: 33px; - font-weight: 800; - color: #303030; - margin: 0; -} - -.stat-icon { - width: 60px; - height: 60px; - flex-shrink: 0; -} - -.stat-label { - font-size: 18px; - font-weight: 500; - color: #627287; - text-align: left; - white-space: nowrap; -} - -.actions-activity-section { - display: flex; - gap: 2rem; - align-items: flex-start; -} - -.quick-actions-container { - display: flex; - flex-direction: column; - flex: 2; - gap: 1.5rem; -} - -.recent-activity-container { - display: flex; - flex-direction: column; - flex: 1; - gap: 1.5rem; -} - -.section-title { - font-size: 30px; - font-weight: 600; - color: #303030; - margin: 0; -} - -.action-card { - display: flex; - align-items: center; - padding: 1.5rem; - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - gap: 1.5rem; -} - -.action-icon { - width: 48px; - height: 48px; - flex-shrink: 0; -} - -.action-content { - display: flex; - flex-direction: column; - flex: 1; -} - -.action-title { - font-size: 23px; - font-weight: 700; - color: #303030; - margin-bottom: 0.25rem; -} - -.action-description { - font-size: 20px; - font-weight: 500; - color: #627287; -} - -.action-button { - padding: 0.625rem 1.5rem; - border-radius: 8px; - font-size: 23px; - font-weight: 600; - cursor: pointer; - border: none; - flex-shrink: 0; -} - -.start-button { - background-color: #4181f5; - color: white; -} - -.start-button:hover { - background-color: #3574e3; -} - -.reset-button { - background-color: white; - color: #303030; - border: 2px solid #ddd; -} - -.reset-button:hover { - background-color: #f5f5f5; -} - -.admin-link { - text-decoration: none; -} - -.admin-manage-button { - display: flex; - align-items: center; - justify-content: center; - gap: 0.65rem; - padding: 0.875rem 2rem; - background-color: #004d40; - color: white; - border: 2px solid #ffc107; - border-radius: 8px; - font-size: 19px; - font-weight: 600; - cursor: pointer; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - white-space: nowrap; -} - -.admin-manage-button:hover { - background-color: #00695c; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.lock-icon { - width: 35px; - height: 35px; - flex-shrink: 0; -} From eff8346f6ea0c9fd9a991e75518a8709282860f0 Mon Sep 17 00:00:00 2001 From: Vera Date: Thu, 13 Nov 2025 05:00:19 +0800 Subject: [PATCH 5/7] Delete user-service/src/test/models/verificationCode 2.test.ts --- .../test/models/verificationCode 2.test.ts | 431 ------------------ 1 file changed, 431 deletions(-) delete mode 100644 user-service/src/test/models/verificationCode 2.test.ts diff --git a/user-service/src/test/models/verificationCode 2.test.ts b/user-service/src/test/models/verificationCode 2.test.ts deleted file mode 100644 index 5ac55e165d..0000000000 --- a/user-service/src/test/models/verificationCode 2.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -import VerificationType from '../../constants/verificationTypes'; -import User from '../../models/user'; -import VerificationCode from '../../models/verificationCode'; - -describe('models/verificationCode', () => { - const TEST_EXPIRY_BEFORE = new Date(Date.now() - 1000000); - const TEST_EXPIRY = new Date(Date.now() + 1000000); - const TEST_EXPIRY_AFTER = new Date(Date.now() + 2000000); - let testUserId; - - beforeEach(async () => { - const user = await User.create({ - username: 'testuser', - email: 'test@example.com', - password: 'password123', - }); - testUserId = user._id; - }); - - describe('VerificationCode creation', () => { - it('should create a verification code with required fields', async () => { - const verificationCode = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - expect(verificationCode.userId).toEqual(testUserId); - expect(verificationCode.type).toBe(VerificationType.VerifyEmail); - expect(verificationCode.expiresAt).toEqual(TEST_EXPIRY); - expect(verificationCode).toHaveProperty('createdAt'); - expect(verificationCode.createdAt).toBeInstanceOf(Date); - }); - - it('should set default createdAt to current time', async () => { - const beforeCreate = Date.now(); - - const verificationCode = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - const afterCreate = Date.now(); - - const createdTime = verificationCode.createdAt.getTime(); - expect(createdTime).toBeGreaterThanOrEqual(beforeCreate); - expect(createdTime).toBeLessThanOrEqual(afterCreate); - }); - - it('should generate unique IDs for verification codes', async () => { - const code1 = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - const code2 = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - expect(code1._id.toString()).not.toBe(code2._id.toString()); - }); - }); - - describe('Verification types', () => { - it('should create VerifyEmail type', async () => { - const code = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - expect(code.type).toBe(VerificationType.VerifyEmail); - }); - - it('should create ResetPassword type', async () => { - const code = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.ResetPassword, - expiresAt: TEST_EXPIRY, - }); - - expect(code.type).toBe(VerificationType.ResetPassword); - }); - - it('should allow different types for same user', async () => { - const emailCode = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - const passwordCode = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.ResetPassword, - expiresAt: TEST_EXPIRY, - }); - - expect(emailCode.type).toBe(VerificationType.VerifyEmail); - expect(passwordCode.type).toBe(VerificationType.ResetPassword); - }); - }); - - describe('Verification validation', () => { - it('should require userId', async () => { - const codeData = { - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }; - - await expect(VerificationCode.create(codeData)).rejects.toThrow(); - }); - - it('should require type', async () => { - const codeData = { - userId: testUserId, - expiresAt: TEST_EXPIRY, - }; - - await expect(VerificationCode.create(codeData)).rejects.toThrow(); - }); - - it('should require expiresAt', async () => { - const codeData = { - userId: testUserId, - type: VerificationType.VerifyEmail, - }; - - await expect(VerificationCode.create(codeData)).rejects.toThrow(); - }); - - it('should require valid ObjectId for userId', async () => { - const codeData = { - userId: 'invalid-id', - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }; - - await expect(VerificationCode.create(codeData)).rejects.toThrow(); - }); - - it('should require date for expiresAt', async () => { - const codeData = { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: 'not-a-date', - }; - - await expect(VerificationCode.create(codeData)).rejects.toThrow(); - }); - - it('should require date for createdAt', async () => { - const codeData = { - userId: testUserId, - type: VerificationType.VerifyEmail, - createdAt: 'not-a-date', - expiresAt: new Date(), - }; - - await expect(VerificationCode.create(codeData)).rejects.toThrow(); - }); - }); - - describe('User reference', () => { - it('should create multiple codes for same user', async () => { - const code1 = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - const code2 = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - expect(code1.userId).toEqual(testUserId); - expect(code2.userId).toEqual(testUserId); - expect(code1._id.toString()).not.toBe(code2._id.toString()); - }); - }); - - describe('VerificationCode queries', () => { - beforeEach(async () => { - const now = Date.now(); - - await VerificationCode.create([ - { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }, - { - userId: testUserId, - type: VerificationType.ResetPassword, - expiresAt: TEST_EXPIRY_AFTER, - }, - { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY_BEFORE, // Expired - }, - ]); - }); - - it('should find all codes for a user', async () => { - const codes = await VerificationCode.find({userId: testUserId}); - - expect(codes.length).toBe(3); - expect(codes.every(c => c.userId.equals(testUserId))).toBe(true); - }); - - it('should find code by Id', async () => { - const createdCode = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - const foundCode = await VerificationCode.findById(createdCode._id); - - expect(foundCode).not.toBeNull(); - expect(foundCode._id.toString()).toBe(createdCode._id.toString()); - }); - - it('should find codes by type', async () => { - const emailCodes = await VerificationCode.find({ - userId: testUserId, - type: VerificationType.VerifyEmail, - }); - - expect(emailCodes.length).toBe(2); - expect(emailCodes.every(c => c.type === VerificationType.VerifyEmail)).toBe(true); - }); - - it('should find valid codes (non-expired)', async () => { - const now = new Date(); - const validCodes = await VerificationCode.find({ - userId: testUserId, - expiresAt: {$gt: now}, - }); - - expect(validCodes.length).toBe(2); - expect(validCodes.every(c => c.expiresAt > now)).toBe(true); - }); - - it('should find expired codes', async () => { - const now = new Date(); - const expiredCodes = await VerificationCode.find({ - userId: testUserId, - expiresAt: {$lte: now}, - }); - - expect(expiredCodes.length).toBe(1); - expect(expiredCodes.every(c => c.expiresAt <= now)).toBe(true); - }); - }); - - describe('VerificationCode deletion', () => { - it('should delete code by ID', async () => { - const code = await VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }); - - await VerificationCode.findByIdAndDelete(code._id); - - const deletedCode = await VerificationCode.findById(code._id); - expect(deletedCode).toBeNull(); - }); - - it('should delete all codes for a user', async () => { - await VerificationCode.create([ - { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }, - { - userId: testUserId, - type: VerificationType.ResetPassword, - expiresAt: TEST_EXPIRY, - }, - ]); - - const result = await VerificationCode.deleteMany({userId: testUserId}); - - expect(result.deletedCount).toBe(2); - - const remainingCodes = await VerificationCode.find({userId: testUserId}); - expect(remainingCodes.length).toBe(0); - }); - - it('should delete expired codes', async () => { - const now = new Date(); - - await VerificationCode.create([ - { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY_BEFORE, - }, - { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: new Date(now), - }, - ]); - - const result = await VerificationCode.deleteMany({ - expiresAt: {$lte: now}, - }); - - expect(result.deletedCount).toBe(2); - }); - - it('should delete codes by type', async () => { - await VerificationCode.create([ - { - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }, - { - userId: testUserId, - type: VerificationType.ResetPassword, - expiresAt: TEST_EXPIRY, - }, - ]); - - const result = await VerificationCode.deleteMany({ - userId: testUserId, - type: VerificationType.VerifyEmail, - }); - - expect(result.deletedCount).toBe(1); - - const remainingCodes = await VerificationCode.find({userId: testUserId}); - expect(remainingCodes.every(c => c.type === VerificationType.ResetPassword)).toBe(true); - }); - }); - - describe('Multiple users', () => { - let user2Id; - let user3Id; - - beforeEach(async () => { - const user2 = await User.create({ - username: 'user2', - email: 'user2@example.com', - password: 'password123', - }); - const user3 = await User.create({ - username: 'user3', - email: 'user3@example.com', - password: 'password123', - }); - user2Id = user2._id; - user3Id = user3._id; - - await VerificationCode.create([ - {userId: testUserId, type: VerificationType.VerifyEmail, expiresAt: TEST_EXPIRY}, - {userId: testUserId, type: VerificationType.ResetPassword, expiresAt: TEST_EXPIRY}, - {userId: user2Id, type: VerificationType.VerifyEmail, expiresAt: TEST_EXPIRY}, - {userId: user3Id, type: VerificationType.ResetPassword, expiresAt: TEST_EXPIRY}, - ]); - }); - - it('should isolate codes by user', async () => { - const user1Codes = await VerificationCode.find({userId: testUserId}); - const user2Codes = await VerificationCode.find({userId: user2Id}); - const user3Codes = await VerificationCode.find({userId: user3Id}); - - expect(user1Codes.length).toBe(2); - expect(user2Codes.length).toBe(1); - expect(user3Codes.length).toBe(1); - }); - - it('should delete only target user codes', async () => { - await VerificationCode.deleteMany({userId: testUserId}); - - const remainingUser1 = await VerificationCode.find({userId: testUserId}); - const remainingUser2 = await VerificationCode.find({userId: user2Id}); - const remainingUser3 = await VerificationCode.find({userId: user3Id}); - - expect(remainingUser1.length).toBe(0); - expect(remainingUser2.length).toBe(1); - expect(remainingUser3.length).toBe(1); - }); - - it('should count codes across all users', async () => { - const totalCodes = await VerificationCode.countDocuments(); - - expect(totalCodes).toBe(4); - }); - - it('should find codes by type across users', async () => { - const emailCodes = await VerificationCode.find({ - type: VerificationType.VerifyEmail, - }); - const passwordCodes = await VerificationCode.find({ - type: VerificationType.ResetPassword, - }); - - expect(emailCodes.length).toBe(2); - expect(passwordCodes.length).toBe(2); - }); - }); - - describe('Others', () => { - it('should handle concurrent code creation', async () => { - const promises = Array.from({length: 10}, () => - VerificationCode.create({ - userId: testUserId, - type: VerificationType.VerifyEmail, - expiresAt: TEST_EXPIRY, - }) - ); - - const codes = await Promise.all(promises); - - expect(codes.length).toBe(10); - expect(new Set(codes.map(c => c._id.toString())).size).toBe(10); - }); - }); -}); From 51ed140424427eda16f2d8d4cd7539c691a8a0d1 Mon Sep 17 00:00:00 2001 From: Vera Date: Thu, 13 Nov 2025 05:00:36 +0800 Subject: [PATCH 6/7] Delete user-service/src/test/models/user 2.test.ts --- user-service/src/test/models/user 2.test.ts | 737 -------------------- 1 file changed, 737 deletions(-) delete mode 100644 user-service/src/test/models/user 2.test.ts diff --git a/user-service/src/test/models/user 2.test.ts b/user-service/src/test/models/user 2.test.ts deleted file mode 100644 index 4467c458cf..0000000000 --- a/user-service/src/test/models/user 2.test.ts +++ /dev/null @@ -1,737 +0,0 @@ -import UserRoleTypes from '../../constants/userRoles'; -import User from '../../models/user'; - -describe('models/User', () => { - const TEST_USERNAME = 'testuser'; - const TEST_USERNAME_CONFLICT = 'tEStUSeR'; - const TEST_USERNAME_DIFFERENT = 'differentuser'; - const TEST_USERNAME_ADMIN = 'admin'; - - const TEST_EMAIL = 'test@example.com'; - const TEST_EMAIL_CONFLICT = 'tESt@eXaMPlE.cOm'; - const TEST_EMAIL_DIFFERENT = 'test_different@example.com'; - const TEST_EMAIL_ADMIN = 'test_admin@admin.com'; - - const TEST_PASSWORD = 'testPassword!@#$%^'; - const TEST_PASSWORD_DIFFERENT = 'differentPassword&^%$#@'; - - const USER = UserRoleTypes.User; - const ADMIN = UserRoleTypes.Admin; - - const TEST_FIRSTNAME = 'John'; - const TEST_LASTNAME = 'Doe'; - const TEST_OCCUPATION = 'information-technology'; - const TEST_AREAOFSTUDY = 'computer-science'; - - const TEST_PROF_PIC_LINK = 'https://example.com/pic.jpg'; - const TEST_PROF_PIC_BASE64 = 'data:image/jpeg;base64, /9j/2woRAYgewoP/9k='; - - const TEST_OAUTHID = '1234567542146357'; - - describe('User creation', () => { - it('should create user with required fields', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const user = await User.create(userData); - - expect(user.username).toBe(userData.username); - expect(user.email).toBe(userData.email); - expect(user.verified).toBe(false); - expect(user.role).toBe(USER); - expect(user.profileComplete).toBe(false); - expect(user.markedForDeletion).toBe(false); - - expect(user.firstName).toBeUndefined(); - expect(user.lastName).toBeUndefined(); - expect(user.occupation).toBeUndefined(); - expect(user.areaOfStudy).toBeUndefined(); - - expect(user.googleOAuthId).toBeUndefined(); - expect(user.googleOAuthEmail).toBeUndefined(); - expect(user.googleOAuthVerified).toBeUndefined(); - - expect(user.githubOAuthId).toBeUndefined(); - expect(user.githubOAuthEmail).toBeUndefined(); - expect(user.githubOAuthVerified).toBeUndefined(); - - expect(user.profilePicture).toBeUndefined(); - expect(user.profilePictureSource).toBeUndefined(); - - expect(user.deletionScheduleAt).toBeUndefined(); - }); - - it('should create a user without password', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - }; - - const user = await User.create(userData); - - expect(user.username).toBe(userData.username); - expect(user.passwordHash).toBeUndefined(); - expect(user.passwordSalt).toBeUndefined(); - expect(user.passwordIterations).toBeUndefined(); - }); - - it('should create a user without email', async () => { - const userData = { - username: TEST_USERNAME, - password: TEST_PASSWORD, - }; - - const user = await User.create(userData); - - expect(user.username).toBe(userData.username); - expect(user.email).toBeUndefined(); - }); - - it('should hash password on creation', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const user = await User.create(userData); - - expect(user).toHaveProperty('passwordHash'); - expect(user).toHaveProperty('passwordSalt'); - expect(user).toHaveProperty('passwordIterations'); - expect(user.hasPassword).toBe(true); - }); - - it('should set timestamps on creation', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const user = await User.create(userData); - - expect(user).toHaveProperty('createdAt'); - expect(user.createdAt).toBeInstanceOf(Date); - expect(user).toHaveProperty('updatedAt'); - expect(user.updatedAt).toBeInstanceOf(Date); - }); - - it('should create user with admin role', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - role: ADMIN, - }; - - const user = await User.create(userData); - - expect(user.role).toBe(ADMIN); - }); - - it('should create verified user', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - verified: true, - }; - - const user = await User.create(userData); - - expect(user.verified).toBe(true); - }); - }); - - describe('Username validation', () => { - it('should require username', async () => { - const userData = { - password: TEST_PASSWORD, - }; - - await expect(User.create(userData)).rejects.toThrow(); - }); - - it('should enforce unique username (case-insensitive)', async () => { - const userData1 = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const userData2 = { - username: TEST_USERNAME_CONFLICT, - email: TEST_EMAIL_DIFFERENT, - password: TEST_PASSWORD, - }; - - await User.create(userData1); - await expect(User.create(userData2)).rejects.toThrow(); - }); - - it('should allow different usernames', async () => { - const userData1 = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const userData2 = { - username: TEST_USERNAME_DIFFERENT, - email: TEST_EMAIL_DIFFERENT, - password: TEST_PASSWORD, - }; - - const user1 = await User.create(userData1); - const user2 = await User.create(userData2); - - expect(user1.username).toBe(TEST_USERNAME); - expect(user2.username).toBe(TEST_USERNAME_DIFFERENT); - }); - }); - - describe('Email validation', () => { - it('should enforce unique email (case-insensitive)', async () => { - const userData1 = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const userData2 = { - username: TEST_USERNAME_DIFFERENT, - email: TEST_EMAIL_CONFLICT, - password: TEST_PASSWORD, - }; - - await User.create(userData1); - await expect(User.create(userData2)).rejects.toThrow(); - }); - - it('should allow same email for undefined/null values', async () => { - const userData1 = { - username: TEST_USERNAME, - password: TEST_PASSWORD, - }; - - const userData2 = { - username: TEST_USERNAME_DIFFERENT, - password: TEST_PASSWORD, - }; - - const user1 = await User.create(userData1); - const user2 = await User.create(userData2); - - expect(user1.email).toBeUndefined(); - expect(user2.email).toBeUndefined(); - }); - - it('should allow different emails', async () => { - const userData1 = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }; - - const userData2 = { - username: TEST_USERNAME_DIFFERENT, - email: TEST_EMAIL_DIFFERENT, - password: TEST_PASSWORD, - }; - - const user1 = await User.create(userData1); - const user2 = await User.create(userData2); - - expect(user1.email).toBe(TEST_EMAIL); - expect(user2.email).toBe(TEST_EMAIL_DIFFERENT); - }); - }); - - describe('Password validation', () => { - describe('setPassword', () => { - it('should generate different hashes for different passwords', async () => { - const user = new User({ - username: TEST_USERNAME, - email: TEST_EMAIL, - }); - - await user.setPassword(TEST_PASSWORD); - const hash1 = user.passwordHash; - - await user.setPassword(TEST_PASSWORD_DIFFERENT); - const hash2 = user.passwordHash; - - expect(hash1).not.toBe(hash2); - }); - - it('should update hasPassword flag', async () => { - const user = new User({ - username: TEST_USERNAME, - email: TEST_EMAIL, - }); - - expect(user.hasPassword).toBe(false); - - await user.setPassword(TEST_PASSWORD); - - expect(user.hasPassword).toBe(true); - }); - }); - - describe('comparePassword', () => { - it('should return true for correct password', async () => { - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }); - - const isValid = await user.comparePassword(TEST_PASSWORD); - expect(isValid).toBe(true); - }); - - it('should return false for incorrect password', async () => { - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }); - - const isValid = await user.comparePassword(TEST_PASSWORD_DIFFERENT); - expect(isValid).toBe(false); - }); - - it('should be case-sensitive', async () => { - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }); - - const isValid = await user.comparePassword(TEST_PASSWORD.toUpperCase()); - expect(isValid).toBe(false); - }); - }); - - describe('Password virtual field', () => { - it('should accept password through virtual field', async () => { - const user = new User({ - username: TEST_USERNAME, - email: TEST_EMAIL, - }); - - (user as any).password = TEST_PASSWORD; - await user.validate(); - - expect(user).toHaveProperty('passwordHash'); - expect(user.hasPassword).toBe(true); - }); - - it('should retrieve password through virtual field', () => { - const user = new User({ - username: TEST_USERNAME, - email: TEST_EMAIL, - }); - - (user as any).password = TEST_PASSWORD; - const retrievedPassword = (user as any).password; - - expect(retrievedPassword).toBe(TEST_PASSWORD); - }); - }); - }); - - describe('Personal ation fields', () => { - it('should store personal information provided', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - firstName: TEST_FIRSTNAME, - lastName: TEST_LASTNAME, - occupation: TEST_OCCUPATION, - areaOfStudy: TEST_AREAOFSTUDY, - profileComplete: true, - }; - - const user = await User.create(userData); - - expect(user.firstName).toBe(TEST_FIRSTNAME); - expect(user.lastName).toBe(TEST_LASTNAME); - expect(user.occupation).toBe(TEST_OCCUPATION); - expect(user.areaOfStudy).toBe(TEST_AREAOFSTUDY); - expect(user.profileComplete).toBe(true); - }); - }); - - describe('OAuth fields', () => { - describe('Google OAuth', () => { - it('should store Google OAuth information', async () => { - const userData = { - username: TEST_USERNAME, - googleOAuthId: TEST_OAUTHID, - googleOAuthEmail: TEST_EMAIL, - googleOAuthVerified: true, - }; - - const user = await User.create(userData); - - expect(user.googleOAuthId).toBe(TEST_OAUTHID); - expect(user.googleOAuthEmail).toBe(TEST_EMAIL); - expect(user.googleOAuthVerified).toBe(true); - }); - }); - - describe('GitHub OAuth', () => { - it('should store GitHub OAuth information', async () => { - const userData = { - username: TEST_USERNAME, - githubOAuthId: TEST_OAUTHID, - githubOAuthEmail: TEST_EMAIL, - githubOAuthVerified: true, - }; - - const user = await User.create(userData); - - expect(user.githubOAuthId).toBe(TEST_OAUTHID); - expect(user.githubOAuthEmail).toBe(TEST_EMAIL); - expect(user.githubOAuthVerified).toBe(true); - }); - }); - - it('should allow both OAuth providers', async () => { - const userData = { - username: TEST_USERNAME, - googleOAuthId: TEST_OAUTHID, - googleOAuthEmail: TEST_EMAIL, - githubOAuthId: TEST_OAUTHID, - githubOAuthEmail: TEST_EMAIL, - }; - - const user = await User.create(userData); - - expect(user.googleOAuthId).toBe(TEST_OAUTHID); - expect(user.githubOAuthId).toBe(TEST_OAUTHID); - }); - }); - - describe('Profile picture', () => { - it('should store profile picture URL', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - profilePicture: TEST_PROF_PIC_LINK, - profilePictureSource: 'google', - }; - - const user = await User.create(userData); - - expect(user.profilePicture).toBe(TEST_PROF_PIC_LINK); - expect(user.profilePictureSource).toBe('google'); - }); - - it('should store base64 encoded image', async () => { - const base64Image = TEST_PROF_PIC_BASE64; - - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - profilePicture: base64Image, - profilePictureSource: 'upload', - }; - - const user = await User.create(userData); - - expect(user.profilePicture).toBe(base64Image); - expect(user.profilePictureSource).toBe('upload'); - }); - }); - - describe('Account deletion fields', () => { - it('should mark account for deletion', async () => { - const deletionDate = new Date(Date.now()); - - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - markedForDeletion: true, - deletionScheduleAt: deletionDate, - }; - - const user = await User.create(userData); - - expect(user.markedForDeletion).toBe(true); - expect(user.deletionScheduleAt).toEqual(deletionDate); - }); - - it('should allow unmarking for deletion', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - markedForDeletion: true, - deletionScheduleAt: new Date(Date.now()), - }; - - const user = await User.create(userData); - expect(user.markedForDeletion).toBe(true); - - user.markedForDeletion = false; - user.deletionScheduleAt = undefined; - await user.save(); - - expect(user.markedForDeletion).toBe(false); - expect(user).toHaveProperty('deletionScheduleAt'); - expect(user.deletionScheduleAt).toBeUndefined(); - }); - }); - - describe('User queries', () => { - const EXPECT_VALID = 2; - const EXPECT_ADMIN = 1; - - beforeEach(async () => { - await User.create([ - { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - verified: true, - }, - { - username: TEST_USERNAME_DIFFERENT, - email: TEST_EMAIL_DIFFERENT, - password: TEST_PASSWORD, - verified: false, - }, - { - username: TEST_USERNAME_ADMIN, - email: TEST_EMAIL_ADMIN, - password: TEST_PASSWORD, - verified: true, - role: ADMIN, - }, - ]); - }); - - it('should find user by username', async () => { - const user = await User.findOne({username: TEST_USERNAME}); - - expect(user).not.toBeNull(); - expect(user.username).toBe(TEST_USERNAME); - }); - - it('should find user by email', async () => { - const user = await User.findOne({email: TEST_EMAIL}); - - expect(user).not.toBeNull(); - expect(user.email).toBe(TEST_EMAIL); - }); - - it('should find user by ID', async () => { - const createdUser = await User.findOne({username: TEST_USERNAME}); - const foundUser = await User.findById(createdUser._id); - - expect(foundUser).not.toBeNull(); - expect(foundUser._id.toString()).toBe(createdUser._id.toString()); - }); - - it('should find verified users', async () => { - const verifiedUsers = await User.find({verified: true}); - - expect(verifiedUsers.length).toBe(EXPECT_VALID); - expect(verifiedUsers.every(u => u.verified)).toBe(true); - }); - - it('should find users by role', async () => { - const admins = await User.find({role: ADMIN}); - - expect(admins.length).toBe(1); - expect(admins.every(u => u.role === ADMIN)).toBe(true); - }); - - it('should support case-insensitive username search', async () => { - const user = await User.findOne({username: TEST_USERNAME_CONFLICT}).collation({ - locale: 'en', - strength: 2, - }); - - expect(user).not.toBeNull(); - expect(user.username).toBe(TEST_USERNAME); - }); - - it('should support case-insensitive email search', async () => { - const user = await User.findOne({email: TEST_EMAIL_CONFLICT}).collation({ - locale: 'en', - strength: 2, - }); - - expect(user).not.toBeNull(); - expect(user.email).toBe(TEST_EMAIL); - }); - }); - - describe('User updates', () => { - it('should update updatedAt timestamp', async () => { - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }); - - const originalUpdatedAt = user.updatedAt; - - user.username = TEST_USERNAME_DIFFERENT; - user.firstName = TEST_FIRSTNAME; - await user.save(); - - await new Promise(resolve => setTimeout(resolve, 10)); - - expect(user.username).toBe(TEST_USERNAME_DIFFERENT); - expect(user.firstName).toBe(TEST_FIRSTNAME); - expect(user.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime()); - }); - - it('should update password correctly', async () => { - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }); - - const oldHash = user.passwordHash; - - (user as any).password = TEST_PASSWORD_DIFFERENT; - await user.save(); - - expect(user.passwordHash).not.toBe(oldHash); - - const isOldValid = await user.comparePassword(TEST_PASSWORD); - const isNewValid = await user.comparePassword(TEST_PASSWORD_DIFFERENT); - - expect(isOldValid).toBe(false); - expect(isNewValid).toBe(true); - }); - }); - - describe('Password rehashing', () => { - it('should warn when password needs rehashing but password not available', async () => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - }); - - user.passwordIterations = 1000; - await user.save(); - - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Password needs rehashing but password not available' - ); - - consoleWarnSpy.mockRestore(); - }); - }); - - describe('toJSON method', () => { - it('should exclude irrelevant fields from JSON', async () => { - const user = await User.create({ - username: TEST_USERNAME, - email: TEST_EMAIL, - verified: true, - password: TEST_PASSWORD, - firstName: TEST_FIRSTNAME, - lastName: TEST_LASTNAME, - occupation: TEST_OCCUPATION, - areaOfStudy: TEST_AREAOFSTUDY, - profileComplete: true, - googleOAuthId: TEST_OAUTHID, - googleOAuthEmail: TEST_EMAIL, - googleOAuthVerified: true, - githubOAuthId: TEST_OAUTHID, - githubOAuthEmail: TEST_EMAIL, - githubOAuthVerified: true, - profilePicture: TEST_PROF_PIC_BASE64, - profilePictureSource: 'upload', - markedForDeletion: true, - deletionScheduleAt: new Date(Date.now()), - }); - - const json = user.toJSON(); - - expect(json.username).toBe(TEST_USERNAME); - expect(json.email).toBe(TEST_EMAIL); - expect(json.verified).toBe(true); - expect(json.firstName).toBe(TEST_FIRSTNAME); - expect(json.lastName).toBe(TEST_LASTNAME); - expect(json.occupation).toBe(TEST_OCCUPATION); - expect(json.areaOfStudy).toBe(TEST_AREAOFSTUDY); - expect(json.profileComplete).toBe(true); - expect(json.googleOAuthId).not.toBe(TEST_OAUTHID); - expect(json.googleOAuthEmail).toBe(TEST_EMAIL); - expect(json.googleOAuthVerified).toBe(true); - expect(json.githubOAuthId).not.toBe(TEST_OAUTHID); - expect(json.githubOAuthEmail).toBe(TEST_EMAIL); - expect(json.githubOAuthVerified).toBe(true); - expect(json.profilePicture).toBe(TEST_PROF_PIC_BASE64); - expect(json.profilePictureSource).toBe('upload'); - - expect(json).not.toHaveProperty('passwordHash'); - expect(json).not.toHaveProperty('passwordSalt'); - expect(json).not.toHaveProperty('passwordIterations'); - expect(json).not.toHaveProperty('markedForDeletion'); - expect(json).not.toHaveProperty('deletionScheduleAt'); - }); - }); - - describe('Others', () => { - it('should handle very long username', async () => { - const longUsername = 'a'.repeat(30); - - const user = await User.create({ - username: longUsername, - email: TEST_USERNAME, - password: TEST_PASSWORD, - }); - - expect(user.username).toBe(longUsername); - }); - - it('should handle unicode characters in names', async () => { - const userData = { - username: TEST_USERNAME, - email: TEST_EMAIL, - password: TEST_PASSWORD, - firstName: "O'Brien", - lastName: 'José-María', - }; - - const user = await User.create(userData); - - expect(user.firstName).toBe("O'Brien"); - expect(user.lastName).toBe('José-María'); - }); - - it('should handle concurrent user creation', async () => { - const users = Array.from({length: 10}, (_, i) => ({ - username: `user${i}`, - email: `user${i}@example.com`, - password: TEST_PASSWORD, - })); - - const createdUsers = await Promise.all(users.map(userData => User.create(userData))); - - expect(createdUsers).toHaveLength(10); - expect(new Set(createdUsers.map(u => u.username)).size).toBe(10); - }); - }); -}); From 9e1f39d484eb7aeac10b7f77bc8e5ed805f63650 Mon Sep 17 00:00:00 2001 From: verakohh Date: Thu, 13 Nov 2025 05:02:18 +0800 Subject: [PATCH 7/7] Revert "Delete frontend/styles/profile.css" This reverts commit bfb5c9ed616077a4f31bea3d81144ac3f3ad86cf. --- frontend/styles/profile.css | 289 ++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 frontend/styles/profile.css diff --git a/frontend/styles/profile.css b/frontend/styles/profile.css new file mode 100644 index 0000000000..106d4b8852 --- /dev/null +++ b/frontend/styles/profile.css @@ -0,0 +1,289 @@ +.verify-prompt-wrapper { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + min-height: 100vh; + padding: 2rem; +} + +.verify-prompt-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; + padding: 3rem 2rem; + border-radius: 0.5rem; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); + max-width: 50rem; + width: 100%; + text-align: center; +} + +.alert-warning { + background-color: #fef3c7; + border: 3px solid #fbbf24; + color: #92400e; +} + +.verify-prompt-content { + display: flex; + flex-direction: column; + text-align: center; + gap: 1rem; +} + +.verify-prompt-title { + font-size: 23px; + font-weight: 500; + color: #1f2937; + margin: 0; +} + +.verify-prompt-message { + font-size: 17px; + line-height: 1.6; + color: #6b7280; + margin: 0; +} + +.verify-prompt-actions { + display: flex; + flex-direction: column; + gap: 2rem; + width: 100%; + align-items: center; +} + +.verify-prompt-note { + font-size: 17px; + color: #6b7280; + margin: 0; +} + +.link-button { + background: none; + border: none; + color: #4285f4; + text-decoration: none; + cursor: pointer; + padding: 0; + font-size: inherit; + transition: all 0.3s ease; +} + +.link-button:hover { + text-decoration: underline; +} + +.profile-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.profile-main { + display: flex; + flex-direction: column; + flex: 1; + padding: 2rem 3rem; + gap: 2rem; +} + +.welcome-section { + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; +} + +.welcome-content { + display: flex; + flex-direction: column; +} + +.welcome-title { + font-size: 45px; + font-weight: 800; + color: #303030; + margin-bottom: 0.5rem; +} + +.welcome-username-highlight { + color: #4181f5; +} + +.welcome-subtitle { + font-size: 23px; + font-weight: 500; + color: #627287; +} + +.stats-section { + display: flex; + flex-wrap: wrap; + gap: 2rem; +} + +.stat-card { + display: flex; + align-items: center; + justify-content: space-between; + flex: 1; + padding: 1.5rem; + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + gap: 1rem; +} + +.stat-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-value { + font-size: 33px; + font-weight: 800; + color: #303030; + margin: 0; +} + +.stat-icon { + width: 60px; + height: 60px; + flex-shrink: 0; +} + +.stat-label { + font-size: 18px; + font-weight: 500; + color: #627287; + text-align: left; + white-space: nowrap; +} + +.actions-activity-section { + display: flex; + gap: 2rem; + align-items: flex-start; +} + +.quick-actions-container { + display: flex; + flex-direction: column; + flex: 2; + gap: 1.5rem; +} + +.recent-activity-container { + display: flex; + flex-direction: column; + flex: 1; + gap: 1.5rem; +} + +.section-title { + font-size: 30px; + font-weight: 600; + color: #303030; + margin: 0; +} + +.action-card { + display: flex; + align-items: center; + padding: 1.5rem; + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + gap: 1.5rem; +} + +.action-icon { + width: 48px; + height: 48px; + flex-shrink: 0; +} + +.action-content { + display: flex; + flex-direction: column; + flex: 1; +} + +.action-title { + font-size: 23px; + font-weight: 700; + color: #303030; + margin-bottom: 0.25rem; +} + +.action-description { + font-size: 20px; + font-weight: 500; + color: #627287; +} + +.action-button { + padding: 0.625rem 1.5rem; + border-radius: 8px; + font-size: 23px; + font-weight: 600; + cursor: pointer; + border: none; + flex-shrink: 0; +} + +.start-button { + background-color: #4181f5; + color: white; +} + +.start-button:hover { + background-color: #3574e3; +} + +.reset-button { + background-color: white; + color: #303030; + border: 2px solid #ddd; +} + +.reset-button:hover { + background-color: #f5f5f5; +} + +.admin-link { + text-decoration: none; +} + +.admin-manage-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.65rem; + padding: 0.875rem 2rem; + background-color: #004d40; + color: white; + border: 2px solid #ffc107; + border-radius: 8px; + font-size: 19px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + white-space: nowrap; +} + +.admin-manage-button:hover { + background-color: #00695c; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.lock-icon { + width: 35px; + height: 35px; + flex-shrink: 0; +}