diff --git a/backend/constants/constants.ts b/backend/constants/constants.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/constants/cornell.ts b/backend/constants/cornell.ts deleted file mode 100644 index 72de526c..00000000 --- a/backend/constants/cornell.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { School, Gender } from '../types'; - -export const CORNELL_SCHOOLS: School[] = [ - 'College of Agriculture and Life Sciences', - 'College of Architecture, Art, and Planning', - 'College of Arts and Sciences', - 'Cornell SC Johnson College of Business', - 'College of Engineering', - 'College of Human Ecology', - 'School of Industrial and Labor Relations', - 'Graduate School', - 'Law School', - 'Brooks School of Public Policy', - 'Weill Cornell Medical', - 'College of Veterinary Medicine', - 'Nolan School of Hotel Administration', -]; - -export const CORNELL_MAJORS: Record = { - 'College of Agriculture and Life Sciences': [ - 'Animal Science', - 'Atmospheric Science', - 'Biological Sciences', - 'Biometry and Statistics', - 'Communication', - 'Development Sociology', - 'Earth and Atmospheric Sciences', - 'Entomology', - 'Environmental and Sustainability Sciences', - 'Food Science', - 'Global Development', - 'Information Science', - 'Landscape Architecture', - 'Natural Resources', - 'Nutritional Sciences', - 'Plant Sciences', - 'Science of Earth Systems', - 'Viticulture and Enology', - ], - 'College of Architecture, Art, and Planning': [ - 'Architecture', - 'Art', - 'City and Regional Planning', - 'Landscape Architecture', - 'Urban and Regional Studies', - ], - 'College of Arts and Sciences': [ - 'Africana Studies', - 'American Studies', - 'Anthropology', - 'Archaeology', - 'Asian Studies', - 'Astronomy', - 'Biology', - 'Biology & Society', - 'Chemistry', - 'China and Asia-Pacific Studies', - 'Classics', - 'Cognitive Science', - 'College Scholar', - 'Comparative Literature', - 'Computer Science', - 'Economics', - 'English', - 'Feminist, Gender, and Sexuality Studies', - 'French', - 'German Studies', - 'Global & Public Health Sciences', - 'Government', - 'History', - 'History of Art', - 'Independent Major', - 'Information Science', - 'Italian', - 'Linguistics', - 'Mathematics', - 'Music', - 'Near Eastern Studies', - 'Performing and Media Arts', - 'Philosophy', - 'Physics', - 'Psychology', - 'Religious Studies', - 'Romance Studies', - 'Russian', - 'Science & Technology Studies', - 'Sociology', - 'Spanish', - 'Statistical Science', - 'Theater, Film, and Dance', - ], - 'Cornell SC Johnson College of Business': [ - 'Applied Economics and Management', - 'Hotel Administration', - 'Business (Dyson)', - 'Real Estate', - ], - 'College of Engineering': [ - 'Biomedical Engineering', - 'Chemical Engineering', - 'Civil Engineering', - 'Computer Science', - 'Electrical and Computer Engineering', - 'Engineering Physics', - 'Environmental Engineering', - 'Information Science, Systems, and Technology', - 'Materials Science and Engineering', - 'Mechanical Engineering', - 'Operations Research and Engineering', - ], - 'College of Human Ecology': [ - 'Design and Environmental Analysis', - 'Fiber Science and Apparel Design', - 'Global and Public Health Sciences', - 'Human Biology, Health, and Society', - 'Human Development', - 'Nutritional Sciences', - 'Policy Analysis and Management', - ], - 'School of Industrial and Labor Relations': [ - 'Industrial and Labor Relations', - ], - 'Graduate School': [ - 'Graduate Student - Arts and Sciences', - 'Graduate Student - Engineering', - 'Graduate Student - Business', - 'Graduate Student - Other', - 'PhD Candidate', - "Master's Student", - ], - 'Law School': ['Law (J.D.)', 'Law (LL.M.)', 'Law (J.S.D.)'], - 'Brooks School of Public Policy': [ - 'Public Policy (MPA)', - 'Public Policy (PhD)', - ], - 'Weill Cornell Medical': [ - 'Medicine (M.D.)', - 'Medical Sciences (PhD)', - 'Physician Assistant Studies', - ], - 'College of Veterinary Medicine': [ - 'Veterinary Medicine (D.V.M.)', - 'Biomedical Sciences (PhD)', - 'Veterinary Graduate', - ], - 'Nolan School of Hotel Administration': ['Hotel Administration'], -}; - -// Flattened list of all majors for easy selection -export const ALL_MAJORS = Object.values(CORNELL_MAJORS).flat().sort(); - -// Academic year classifications -export type Year = - | 'Freshman' - | 'Sophomore' - | 'Junior' - | 'Senior' - | 'Graduate' - | 'PhD' - | 'Post-Doc'; - -export const YEARS: Year[] = [ - 'Freshman', - 'Sophomore', - 'Junior', - 'Senior', - 'Graduate', - 'PhD', - 'Post-Doc', -]; - -// Default preferences for new users -export const DEFAULT_PREFERENCES = { - ageRange: { min: 18, max: 25 }, - years: ['Freshman', 'Sophomore', 'Junior', 'Senior', 'Graduate'] as Year[], - schools: CORNELL_SCHOOLS, - majors: [], // Empty means all majors - genders: [] as Gender[], // User must set explicitly, originally in onboarding process -}; diff --git a/backend/functions/src/constants/cornell.ts b/backend/functions/src/constants/cornell.ts deleted file mode 100644 index f055140e..00000000 --- a/backend/functions/src/constants/cornell.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { School, Gender } from "../types"; - -export const CORNELL_SCHOOLS: School[] = [ - "College of Agriculture and Life Sciences", - "College of Architecture, Art, and Planning", - "College of Arts and Sciences", - "Cornell SC Johnson College of Business", - "College of Engineering", - "College of Human Ecology", - "School of Industrial and Labor Relations", - "Graduate School", - "Law School", - "Brooks School of Public Policy", - "Weill Cornell Medical", - "College of Veterinary Medicine", - "Nolan School of Hotel Administration", -]; - -export const CORNELL_MAJORS: Record = { - "College of Agriculture and Life Sciences": [ - "Animal Science", - "Atmospheric Science", - "Biological Sciences", - "Biometry and Statistics", - "Communication", - "Development Sociology", - "Earth and Atmospheric Sciences", - "Entomology", - "Environmental and Sustainability Sciences", - "Food Science", - "Global Development", - "Information Science", - "Landscape Architecture", - "Natural Resources", - "Nutritional Sciences", - "Plant Sciences", - "Science of Earth Systems", - "Viticulture and Enology", - ], - "College of Architecture, Art, and Planning": [ - "Architecture", - "Art", - "City and Regional Planning", - "Landscape Architecture", - "Urban and Regional Studies", - ], - "College of Arts and Sciences": [ - "Africana Studies", - "American Studies", - "Anthropology", - "Archaeology", - "Asian Studies", - "Astronomy", - "Biology", - "Biology & Society", - "Chemistry", - "China and Asia-Pacific Studies", - "Classics", - "Cognitive Science", - "College Scholar", - "Comparative Literature", - "Computer Science", - "Economics", - "English", - "Feminist, Gender, and Sexuality Studies", - "French", - "German Studies", - "Global & Public Health Sciences", - "Government", - "History", - "History of Art", - "Independent Major", - "Information Science", - "Italian", - "Linguistics", - "Mathematics", - "Music", - "Near Eastern Studies", - "Performing and Media Arts", - "Philosophy", - "Physics", - "Psychology", - "Religious Studies", - "Romance Studies", - "Russian", - "Science & Technology Studies", - "Sociology", - "Spanish", - "Statistical Science", - "Theater, Film, and Dance", - ], - "Cornell SC Johnson College of Business": [ - "Applied Economics and Management", - "Hotel Administration", - "Business (Dyson)", - "Real Estate", - ], - "College of Engineering": [ - "Biomedical Engineering", - "Chemical Engineering", - "Civil Engineering", - "Computer Science", - "Electrical and Computer Engineering", - "Engineering Physics", - "Environmental Engineering", - "Information Science, Systems, and Technology", - "Materials Science and Engineering", - "Mechanical Engineering", - "Operations Research and Engineering", - ], - "College of Human Ecology": [ - "Design and Environmental Analysis", - "Fiber Science and Apparel Design", - "Global and Public Health Sciences", - "Human Biology, Health, and Society", - "Human Development", - "Nutritional Sciences", - "Policy Analysis and Management", - ], - "School of Industrial and Labor Relations": [ - "Industrial and Labor Relations", - ], - "Graduate School": [ - "Graduate Student - Arts and Sciences", - "Graduate Student - Engineering", - "Graduate Student - Business", - "Graduate Student - Other", - "PhD Candidate", - "Master's Student", - ], - "Law School": ["Law (J.D.)", "Law (LL.M.)", "Law (J.S.D.)"], - "Brooks School of Public Policy": [ - "Public Policy (MPA)", - "Public Policy (PhD)", - ], - "Weill Cornell Medical": [ - "Medicine (M.D.)", - "Medical Sciences (PhD)", - "Physician Assistant Studies", - ], - "College of Veterinary Medicine": [ - "Veterinary Medicine (D.V.M.)", - "Biomedical Sciences (PhD)", - "Veterinary Graduate", - ], - "Nolan School of Hotel Administration": ["Hotel Administration"], -}; - -// Flattened list of all majors for easy selection -export const ALL_MAJORS = Object.values(CORNELL_MAJORS).flat().sort(); - -// Academic year classifications -export type Year = - | "Freshman" - | "Sophomore" - | "Junior" - | "Senior" - | "Graduate" - | "PhD" - | "Post-Doc"; - -export const YEARS: Year[] = [ - "Freshman", - "Sophomore", - "Junior", - "Senior", - "Graduate", - "PhD", - "Post-Doc", -]; - -// Default preferences for new users -export const DEFAULT_PREFERENCES = { - ageRange: { min: 18, max: 25 }, - years: ["Freshman", "Sophomore", "Junior", "Senior", "Graduate"] as Year[], - schools: CORNELL_SCHOOLS, - majors: [], // Empty means all majors - genders: [] as Gender[], // User must set explicitly, originally in onboarding process -}; diff --git a/backend/scripts/get-user-emails.ts b/backend/scripts/get-user-emails.ts index 4cf634e3..decebf7d 100644 --- a/backend/scripts/get-user-emails.ts +++ b/backend/scripts/get-user-emails.ts @@ -50,7 +50,9 @@ async function getAllUserEmails() { }); if (skippedEmails.length > 0) { - console.log(`Skipped ${skippedEmails.length} test accounts (no numbers in netid):`); + console.log( + `Skipped ${skippedEmails.length} test accounts (no numbers in netid):` + ); console.log(skippedEmails.join(', ')); console.log(); } diff --git a/backend/scripts/investigate-matches.ts b/backend/scripts/investigate-matches.ts index 936efe9f..520cd1fa 100644 --- a/backend/scripts/investigate-matches.ts +++ b/backend/scripts/investigate-matches.ts @@ -41,7 +41,11 @@ async function investigateMatches() { // Data structures for analysis const allMatches = new Map(); const usersWithNullMatches: string[] = []; - const nullMatchDetails: Array<{ netid: string; matches: string[]; nullPositions: number[] }> = []; + const nullMatchDetails: Array<{ + netid: string; + matches: string[]; + nullPositions: number[]; + }> = []; // Parse all match documents matchesSnapshot.docs.forEach((doc) => { @@ -51,7 +55,13 @@ async function investigateMatches() { // Check for null/empty matches const nullPositions: number[] = []; data.matches.forEach((match, index) => { - if (match === null || match === 'null' || match === undefined || match === '' || !match) { + if ( + match === null || + match === 'null' || + match === undefined || + match === '' || + !match + ) { nullPositions.push(index); } }); @@ -61,14 +71,16 @@ async function investigateMatches() { nullMatchDetails.push({ netid: data.netid, matches: data.matches, - nullPositions + nullPositions, }); } }); console.log('═══════════════════════════════════════════════════════════'); console.log('NULL MATCHES ANALYSIS'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); console.log(`❌ Users with null matches: ${usersWithNullMatches.length}`); @@ -86,10 +98,16 @@ async function investigateMatches() { console.log('═══════════════════════════════════════════════════════════'); console.log('MUTUALITY ANALYSIS'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); // Check for non-mutual matches - const nonMutualPairs: Array<{ userA: string; userB: string; direction: string }> = []; + const nonMutualPairs: Array<{ + userA: string; + userB: string; + direction: string; + }> = []; const checkedPairs = new Set(); allMatches.forEach((matchDocA, netidA) => { @@ -108,11 +126,13 @@ async function investigateMatches() { const matchDocB = allMatches.get(netidB); if (!matchDocB) { - console.log(` ⚠️ User ${netidB} (matched with ${netidA}) has no match document`); + console.log( + ` ⚠️ User ${netidB} (matched with ${netidA}) has no match document` + ); nonMutualPairs.push({ userA: netidA, userB: netidB, - direction: `${netidA} → ${netidB} (no reciprocal document)` + direction: `${netidA} → ${netidB} (no reciprocal document)`, }); return; } @@ -123,7 +143,7 @@ async function investigateMatches() { nonMutualPairs.push({ userA: netidA, userB: netidB, - direction: `${netidA} → ${netidB} (but ${netidB} ↛ ${netidA})` + direction: `${netidA} → ${netidB} (but ${netidB} ↛ ${netidA})`, }); } }); @@ -143,12 +163,16 @@ async function investigateMatches() { console.log('═══════════════════════════════════════════════════════════'); console.log('MATCH DISTRIBUTION ANALYSIS'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); // Analyze match counts const matchCounts = new Map(); allMatches.forEach((matchDoc) => { - const validMatches = matchDoc.matches.filter(m => m && m !== 'null').length; + const validMatches = matchDoc.matches.filter( + (m) => m && m !== 'null' + ).length; matchCounts.set(validMatches, (matchCounts.get(validMatches) || 0) + 1); }); @@ -159,9 +183,13 @@ async function investigateMatches() { console.log(` ${count} matches: ${users} users`); }); - console.log('\n═══════════════════════════════════════════════════════════'); + console.log( + '\n═══════════════════════════════════════════════════════════' + ); console.log('SAMPLE MATCH DATA'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); // Show first 5 match documents as examples let sampleCount = 0; @@ -177,7 +205,9 @@ async function investigateMatches() { console.log('═══════════════════════════════════════════════════════════'); console.log('SUMMARY'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); console.log(`Total users in 2025_46: ${allMatches.size}`); console.log(`Users with null matches: ${usersWithNullMatches.length}`); @@ -189,7 +219,9 @@ async function investigateMatches() { } else { console.log('❌ BUGS CONFIRMED:'); if (usersWithNullMatches.length > 0) { - console.log(` - ${usersWithNullMatches.length} users have null matches`); + console.log( + ` - ${usersWithNullMatches.length} users have null matches` + ); } if (nonMutualPairs.length > 0) { console.log(` - ${nonMutualPairs.length} non-mutual match pairs`); @@ -205,9 +237,8 @@ async function investigateMatches() { nullMatchDetails, nonMutualPairs, matchCounts, - allMatches + allMatches, }; - } catch (error) { console.error('❌ Error during investigation:', error); throw error; diff --git a/backend/scripts/list-prompts-and-matches.ts b/backend/scripts/list-prompts-and-matches.ts index 76179387..1d33f818 100644 --- a/backend/scripts/list-prompts-and-matches.ts +++ b/backend/scripts/list-prompts-and-matches.ts @@ -11,7 +11,9 @@ async function listPromptsAndMatches() { // List all prompts console.log('═══════════════════════════════════════════════════════════'); console.log('WEEKLY PROMPTS'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); const promptsSnapshot = await db .collection('weeklyPrompts') @@ -27,14 +29,18 @@ async function listPromptsAndMatches() { console.log(` Question: ${data.question?.substring(0, 60)}...`); console.log(` Status: ${data.status || 'unknown'}`); console.log(` Active: ${data.active}`); - console.log(` Created: ${data.createdAt?.toDate().toISOString().split('T')[0]}`); + console.log( + ` Created: ${data.createdAt?.toDate().toISOString().split('T')[0]}` + ); console.log(''); }); // List some match documents console.log('═══════════════════════════════════════════════════════════'); console.log('WEEKLY MATCHES SAMPLE'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); const matchesSnapshot = await db .collection('weeklyMatches') @@ -55,17 +61,22 @@ async function listPromptsAndMatches() { }); // Get count of all matches - const allMatchesSnapshot = await db.collection('weeklyMatches').count().get(); - console.log(`\nTotal match documents in database: ${allMatchesSnapshot.data().count}\n`); + const allMatchesSnapshot = await db + .collection('weeklyMatches') + .count() + .get(); + console.log( + `\nTotal match documents in database: ${allMatchesSnapshot.data().count}\n` + ); // Group matches by promptId console.log('═══════════════════════════════════════════════════════════'); console.log('MATCHES BY PROMPT ID'); - console.log('═══════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════\n' + ); - const allMatchesForGrouping = await db - .collection('weeklyMatches') - .get(); + const allMatchesForGrouping = await db.collection('weeklyMatches').get(); const matchesByPrompt = new Map(); allMatchesForGrouping.docs.forEach((doc) => { @@ -81,7 +92,6 @@ async function listPromptsAndMatches() { }); console.log('\n🔍 Listing complete!\n'); - } catch (error) { console.error('❌ Error during listing:', error); throw error; diff --git a/backend/scripts/setAdminByEmail.ts b/backend/scripts/setAdminByEmail.ts index 849fddd7..dc7e0f48 100644 --- a/backend/scripts/setAdminByEmail.ts +++ b/backend/scripts/setAdminByEmail.ts @@ -37,7 +37,9 @@ for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.error(`❌ Error: Missing required environment variable: ${envVar}`); console.error('\nUsage:'); - console.error(' ADMIN_EMAIL=user@cornell.edu npx ts-node scripts/setAdminByEmail.ts'); + console.error( + ' ADMIN_EMAIL=user@cornell.edu npx ts-node scripts/setAdminByEmail.ts' + ); console.error('\nOr add to your .env file:'); console.error(' ADMIN_EMAIL=user@cornell.edu'); console.error(' FIREBASE_PROJECT_ID=your-project-id'); @@ -120,9 +122,7 @@ async function setupAdmin() { console.log('⚠️ Admin document already exists in Firestore'); const existingData = adminDoc.data(); console.log(' Existing data:', JSON.stringify(existingData, null, 2)); - console.log( - '\n❓ Keeping existing document (not updating)\n' - ); + console.log('\n❓ Keeping existing document (not updating)\n'); } else { // Step 6: Create admin document in Firestore console.log('📝 Step 6: Creating admin document in Firestore...'); diff --git a/backend/src/__tests__/deviceTokenService.test.ts b/backend/src/__tests__/deviceTokenService.test.ts index bec1d1e0..445e9177 100644 --- a/backend/src/__tests__/deviceTokenService.test.ts +++ b/backend/src/__tests__/deviceTokenService.test.ts @@ -51,7 +51,9 @@ describe('Device Token Service', () => { (db.collection as jest.Mock) = mockCollection; // Mock Expo.isExpoPushToken - (Expo.isExpoPushToken as unknown as jest.Mock) = jest.fn().mockReturnValue(true); + (Expo.isExpoPushToken as unknown as jest.Mock) = jest + .fn() + .mockReturnValue(true); }); describe('registerPushToken', () => { diff --git a/backend/src/__tests__/integration/endToEnd.integration.test.ts b/backend/src/__tests__/integration/endToEnd.integration.test.ts index 98161b5b..0cbd287e 100644 --- a/backend/src/__tests__/integration/endToEnd.integration.test.ts +++ b/backend/src/__tests__/integration/endToEnd.integration.test.ts @@ -12,7 +12,11 @@ * - Full workflow scenarios */ -import { generateMatchesForPrompt, revealMatch, createWeeklyMatch } from '../../services/matchingService'; +import { + generateMatchesForPrompt, + revealMatch, + createWeeklyMatch, +} from '../../services/matchingService'; import { createNudge, getNudgeStatus } from '../../services/nudgesService'; import { createTestUsers, @@ -84,7 +88,9 @@ describe('End-to-End Integration Tests', () => { const matches = await getUserMatches(user.netid, testPromptId); expect(matches).toBeTruthy(); if (!matches) { - throw new Error(`No matches found for user ${user.netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${user.netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(matches.matches.length).toBeGreaterThanOrEqual(1); expect(matches.matches.length).toBeLessThanOrEqual(3); @@ -93,16 +99,25 @@ describe('End-to-End Integration Tests', () => { // Step 6: User 1 nudges User 2 console.log('Step 6: User 1 nudging User 2...'); - const user1Matches = await getUserMatches(testUsers[0].netid, testPromptId); + const user1Matches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!user1Matches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } const user2Netid = user1Matches.matches[0]; await createNudge(testUsers[0].netid, user2Netid, testPromptId); // Verify nudge was created - const nudge1to2 = await getNudge(testUsers[0].netid, user2Netid, testPromptId); + const nudge1to2 = await getNudge( + testUsers[0].netid, + user2Netid, + testPromptId + ); expect(nudge1to2).toBeTruthy(); expect(nudge1to2!.mutual).toBe(false); @@ -111,24 +126,41 @@ describe('End-to-End Integration Tests', () => { await createNudge(user2Netid, testUsers[0].netid, testPromptId); // Verify mutual nudge (refetch both nudges after mutual nudge is created) - const nudge2to1 = await getNudge(user2Netid, testUsers[0].netid, testPromptId); - const nudge1to2Updated = await getNudge(testUsers[0].netid, user2Netid, testPromptId); + const nudge2to1 = await getNudge( + user2Netid, + testUsers[0].netid, + testPromptId + ); + const nudge1to2Updated = await getNudge( + testUsers[0].netid, + user2Netid, + testPromptId + ); expect(nudge2to1!.mutual).toBe(true); expect(nudge1to2Updated!.mutual).toBe(true); // Step 8: Verify chat is unlocked between them console.log('Step 8: Verifying chat unlock...'); - const user1MatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); + const user1MatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const user2MatchesAfter = await getUserMatches(user2Netid, testPromptId); if (!user1MatchesAfter) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } if (!user2MatchesAfter) { - throw new Error(`No matches found for user ${user2Netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${user2Netid} on prompt ${testPromptId}. Check test setup.` + ); } const indexOfUser2InUser1 = user1MatchesAfter.matches.indexOf(user2Netid); - const indexOfUser1InUser2 = user2MatchesAfter.matches.indexOf(testUsers[0].netid); + const indexOfUser1InUser2 = user2MatchesAfter.matches.indexOf( + testUsers[0].netid + ); expect(user1MatchesAfter.chatUnlocked![indexOfUser2InUser1]).toBe(true); expect(user2MatchesAfter.chatUnlocked![indexOfUser1InUser2]).toBe(true); @@ -137,9 +169,14 @@ describe('End-to-End Integration Tests', () => { console.log('Step 9: User 1 revealing User 3...'); await revealMatch(testUsers[0].netid, testPromptId, 1); - const user1MatchesFinal = await getUserMatches(testUsers[0].netid, testPromptId); + const user1MatchesFinal = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!user1MatchesFinal) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(user1MatchesFinal.revealed[1]).toBe(true); @@ -151,7 +188,9 @@ describe('End-to-End Integration Tests', () => { const matches = await getUserMatches(user.netid, testPromptId); expect(matches).toBeTruthy(); if (!matches) { - throw new Error(`No matches found for user ${user.netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${user.netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(matches.matches.length).toBeGreaterThan(0); expect(matches.matches.length).toBeLessThanOrEqual(3); @@ -183,10 +222,15 @@ describe('End-to-End Integration Tests', () => { ).resolves.toBeDefined(); // But since user already has 3 matches, no new match should be added - const matchesAfterAttempt = await getUserMatches(testUsers[0].netid, testPromptId); + const matchesAfterAttempt = await getUserMatches( + testUsers[0].netid, + testPromptId + ); expect(matchesAfterAttempt).toBeTruthy(); if (!matchesAfterAttempt) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(matchesAfterAttempt.matches.length).toBeGreaterThanOrEqual(1); expect(matchesAfterAttempt.matches.length).toBeLessThanOrEqual(3); @@ -202,17 +246,31 @@ describe('End-to-End Integration Tests', () => { testPromptId = prompt.promptId; // Manually create bidirectional matches - await createWeeklyMatch(testUsers[0].netid, testPromptId, [testUsers[1].netid]); - await createWeeklyMatch(testUsers[1].netid, testPromptId, [testUsers[0].netid]); + await createWeeklyMatch(testUsers[0].netid, testPromptId, [ + testUsers[1].netid, + ]); + await createWeeklyMatch(testUsers[1].netid, testPromptId, [ + testUsers[0].netid, + ]); // Verify matches were created - const user0Matches = await getUserMatches(testUsers[0].netid, testPromptId); - const user1Matches = await getUserMatches(testUsers[1].netid, testPromptId); + const user0Matches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); + const user1Matches = await getUserMatches( + testUsers[1].netid, + testPromptId + ); if (!user0Matches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } if (!user1Matches) { - throw new Error(`No matches found for user ${testUsers[1].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[1].netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(user0Matches.matches).toContain(testUsers[1].netid); @@ -223,13 +281,23 @@ describe('End-to-End Integration Tests', () => { await createNudge(testUsers[1].netid, testUsers[0].netid, testPromptId); // Verify chat unlocked - const user0MatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); - const user1MatchesAfter = await getUserMatches(testUsers[1].netid, testPromptId); + const user0MatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); + const user1MatchesAfter = await getUserMatches( + testUsers[1].netid, + testPromptId + ); if (!user0MatchesAfter) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } if (!user1MatchesAfter) { - throw new Error(`No matches found for user ${testUsers[1].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[1].netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(user0MatchesAfter.chatUnlocked![0]).toBe(true); @@ -262,11 +330,15 @@ describe('End-to-End Integration Tests', () => { expect(matches1).toBeTruthy(); if (!matches1) { - throw new Error(`No matches found for user ${user.netid} on prompt ${prompt1.promptId}. Check test setup.`); + throw new Error( + `No matches found for user ${user.netid} on prompt ${prompt1.promptId}. Check test setup.` + ); } expect(matches2).toBeTruthy(); if (!matches2) { - throw new Error(`No matches found for user ${user.netid} on prompt ${prompt2.promptId}. Check test setup.`); + throw new Error( + `No matches found for user ${user.netid} on prompt ${prompt2.promptId}. Check test setup.` + ); } expect(matches1.promptId).toBe(prompt1.promptId); @@ -274,9 +346,14 @@ describe('End-to-End Integration Tests', () => { } // Nudge in prompt 1 shouldn't affect prompt 2 - const user0Prompt1Matches = await getUserMatches(testUsers[0].netid, prompt1.promptId); + const user0Prompt1Matches = await getUserMatches( + testUsers[0].netid, + prompt1.promptId + ); if (!user0Prompt1Matches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${prompt1.promptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${prompt1.promptId}. Check test setup.` + ); } const matchedUser = user0Prompt1Matches.matches[0]; @@ -284,13 +361,23 @@ describe('End-to-End Integration Tests', () => { await createNudge(matchedUser, testUsers[0].netid, prompt1.promptId); // Chat should be unlocked for prompt1 but not prompt2 - const user0Prompt1After = await getUserMatches(testUsers[0].netid, prompt1.promptId); - const user0Prompt2After = await getUserMatches(testUsers[0].netid, prompt2.promptId); + const user0Prompt1After = await getUserMatches( + testUsers[0].netid, + prompt1.promptId + ); + const user0Prompt2After = await getUserMatches( + testUsers[0].netid, + prompt2.promptId + ); if (!user0Prompt1After) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${prompt1.promptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${prompt1.promptId}. Check test setup.` + ); } if (!user0Prompt2After) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${prompt2.promptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${prompt2.promptId}. Check test setup.` + ); } expect(user0Prompt1After.chatUnlocked).toBeDefined(); @@ -298,7 +385,9 @@ describe('End-to-End Integration Tests', () => { // Prompt 2 chat should not be affected if (user0Prompt2After.chatUnlocked) { - expect(user0Prompt2After.chatUnlocked.every((u: boolean) => u === false)).toBe(true); + expect( + user0Prompt2After.chatUnlocked.every((u: boolean) => u === false) + ).toBe(true); } console.log('✅ Multiple prompts work independently!'); @@ -315,9 +404,14 @@ describe('End-to-End Integration Tests', () => { await generateMatchesForPrompt(testPromptId); // Find three users who are all matched with each other - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!userAMatches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } const userBNetid = userAMatches.matches[0]; const userCNetid = userAMatches.matches[1]; @@ -326,10 +420,14 @@ describe('End-to-End Integration Tests', () => { const userBMatches = await getUserMatches(userBNetid, testPromptId); const userCMatches = await getUserMatches(userCNetid, testPromptId); if (!userBMatches) { - throw new Error(`No matches found for user ${userBNetid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${userBNetid} on prompt ${testPromptId}. Check test setup.` + ); } if (!userCMatches) { - throw new Error(`No matches found for user ${userCNetid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${userCNetid} on prompt ${testPromptId}. Check test setup.` + ); } // A-B mutual nudge @@ -356,7 +454,9 @@ describe('End-to-End Integration Tests', () => { // Verify User A has chat unlocked with both B and C const userAFinal = await getUserMatches(testUsers[0].netid, testPromptId); if (!userAFinal) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } const indexOfB = userAFinal.matches.indexOf(userBNetid); const indexOfC = userAFinal.matches.indexOf(userCNetid); @@ -376,9 +476,14 @@ describe('End-to-End Integration Tests', () => { await generateMatchesForPrompt(testPromptId); // User 0 reveals all their matches (could be 1-3) - const matchesBeforeReveal = await getUserMatches(testUsers[0].netid, testPromptId); + const matchesBeforeReveal = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!matchesBeforeReveal) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } // Reveal all matches dynamically @@ -386,12 +491,19 @@ describe('End-to-End Integration Tests', () => { await revealMatch(testUsers[0].netid, testPromptId, i); } - const matchesAfterReveal = await getUserMatches(testUsers[0].netid, testPromptId); + const matchesAfterReveal = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!matchesAfterReveal) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } // All matches should be revealed - expect(matchesAfterReveal.revealed.every((r: boolean) => r === true)).toBe(true); + expect( + matchesAfterReveal.revealed.every((r: boolean) => r === true) + ).toBe(true); // Now nudge one of the revealed matches const matchedUser = matchesAfterReveal.matches[0]; @@ -399,19 +511,26 @@ describe('End-to-End Integration Tests', () => { await createNudge(matchedUser, testUsers[0].netid, testPromptId); // Both revealed and chatUnlocked should be maintained - const matchesFinal = await getUserMatches(testUsers[0].netid, testPromptId); + const matchesFinal = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!matchesFinal) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } // All matches should still be revealed - expect(matchesFinal.revealed.every((r: boolean) => r === true)).toBe(true); + expect(matchesFinal.revealed.every((r: boolean) => r === true)).toBe( + true + ); // The first match should have chat unlocked (mutual nudge) expect(matchesFinal.chatUnlocked![0]).toBe(true); console.log('✅ Revealing before nudging works correctly!'); }); - test('Scenario: Asymmetric nudging (some users nudge, others don\'t)', async () => { + test("Scenario: Asymmetric nudging (some users nudge, others don't)", async () => { testUsers = await createTestUsers(6); const prompt = await createTestPrompt(); testPromptId = prompt.promptId; @@ -419,9 +538,14 @@ describe('End-to-End Integration Tests', () => { await createTestPromptAnswers(testUsers, testPromptId); await generateMatchesForPrompt(testPromptId); - const user0Matches = await getUserMatches(testUsers[0].netid, testPromptId); + const user0Matches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!user0Matches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } // Ensure user has at least 2 matches for asymmetric nudging test @@ -441,7 +565,9 @@ describe('End-to-End Integration Tests', () => { // Check nudge statuses const statuses = []; for (let i = 0; i < matchCount; i++) { - statuses.push(await getNudgeStatus(testUsers[0].netid, matches[i], testPromptId)); + statuses.push( + await getNudgeStatus(testUsers[0].netid, matches[i], testPromptId) + ); } // First match should be mutual, others should not @@ -453,7 +579,9 @@ describe('End-to-End Integration Tests', () => { // Only chat with first match should be unlocked const user0Final = await getUserMatches(testUsers[0].netid, testPromptId); if (!user0Final) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } expect(user0Final.chatUnlocked![0]).toBe(true); @@ -478,7 +606,9 @@ describe('End-to-End Integration Tests', () => { const operations = async () => { const matches = await getUserMatches(testUsers[0].netid, testPromptId); if (!matches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } // Reveal all matches dynamically (user may have 1-3 matches) @@ -494,9 +624,14 @@ describe('End-to-End Integration Tests', () => { await operations(); // Verify data integrity - const finalMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const finalMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); if (!finalMatches) { - throw new Error(`No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.`); + throw new Error( + `No matches found for user ${testUsers[0].netid} on prompt ${testPromptId}. Check test setup.` + ); } // Should have 1-3 matches @@ -508,7 +643,9 @@ describe('End-to-End Integration Tests', () => { // chatUnlocked array should exist and match matches array length expect(finalMatches.chatUnlocked).toBeDefined(); - expect(finalMatches.chatUnlocked).toHaveLength(finalMatches.matches.length); + expect(finalMatches.chatUnlocked).toHaveLength( + finalMatches.matches.length + ); // All matches should be unique const uniqueMatches = new Set(finalMatches.matches); diff --git a/backend/src/__tests__/integration/matching.integration.test.ts b/backend/src/__tests__/integration/matching.integration.test.ts index 51055164..1103a9b2 100644 --- a/backend/src/__tests__/integration/matching.integration.test.ts +++ b/backend/src/__tests__/integration/matching.integration.test.ts @@ -89,7 +89,10 @@ describe('Matching Algorithm Integration Tests', () => { // Verify document ID format: ${netid}_${promptId} for (const user of testUsers) { const expectedDocId = `${user.netid}_${testPromptId}`; - const doc = await db.collection('weeklyMatches').doc(expectedDocId).get(); + const doc = await db + .collection('weeklyMatches') + .doc(expectedDocId) + .get(); expect(doc.exists).toBe(true); const data = doc.data(); @@ -198,7 +201,8 @@ describe('Matching Algorithm Integration Tests', () => { expect(expiresAt.getTime()).toBeGreaterThan(Date.now()); // Should be within 7 days - const daysDiff = (expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24); + const daysDiff = + (expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24); expect(daysDiff).toBeLessThanOrEqual(7); expect(daysDiff).toBeGreaterThan(0); }); @@ -239,8 +243,14 @@ describe('Matching Algorithm Integration Tests', () => { expect(matchedCount).toBe(2); // Each user should match with the other (only 1 match available) - const user1Matches = await getUserMatches(testUsers[0].netid, testPromptId); - const user2Matches = await getUserMatches(testUsers[1].netid, testPromptId); + const user1Matches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); + const user2Matches = await getUserMatches( + testUsers[1].netid, + testPromptId + ); expect(user1Matches?.matches).toContain(testUsers[1].netid); expect(user2Matches?.matches).toContain(testUsers[0].netid); @@ -314,15 +324,25 @@ describe('Matching Algorithm Integration Tests', () => { await generateMatchesForPrompt(prompt2.promptId); // Each user should have matches for both prompts - const user1Matches1 = await getUserMatches(testUsers[0].netid, prompt1.promptId); - const user1Matches2 = await getUserMatches(testUsers[0].netid, prompt2.promptId); + const user1Matches1 = await getUserMatches( + testUsers[0].netid, + prompt1.promptId + ); + const user1Matches2 = await getUserMatches( + testUsers[0].netid, + prompt2.promptId + ); // Verify both match sets exist if (!user1Matches1) { - throw new Error(`No matches found for user on prompt1: ${prompt1.promptId}`); + throw new Error( + `No matches found for user on prompt1: ${prompt1.promptId}` + ); } if (!user1Matches2) { - throw new Error(`No matches found for user on prompt2: ${prompt2.promptId}`); + throw new Error( + `No matches found for user on prompt2: ${prompt2.promptId}` + ); } expect(user1Matches1).toBeTruthy(); @@ -358,8 +378,14 @@ describe('Matching Algorithm Integration Tests', () => { expect(matchedCount).toBeGreaterThan(0); // User0 and User1 have identical interests, should likely match - const user0Matches = await getUserMatches(testUsers[0].netid, testPromptId); - const user1Matches = await getUserMatches(testUsers[1].netid, testPromptId); + const user0Matches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); + const user1Matches = await getUserMatches( + testUsers[1].netid, + testPromptId + ); // Due to high compatibility (shared interests), they should match each other expect(user0Matches).toBeTruthy(); diff --git a/backend/src/__tests__/integration/nudging.integration.test.ts b/backend/src/__tests__/integration/nudging.integration.test.ts index eb636f7a..ba4856b9 100644 --- a/backend/src/__tests__/integration/nudging.integration.test.ts +++ b/backend/src/__tests__/integration/nudging.integration.test.ts @@ -11,18 +11,18 @@ * - Correct match index updates in chatUnlocked array */ -import { createNudge, getNudgeStatus } from '../../services/nudgesService'; +import { db } from '../../../firebaseAdmin'; import { generateMatchesForPrompt } from '../../services/matchingService'; +import { createNudge, getNudgeStatus } from '../../services/nudgesService'; import { - createTestUsers, + TestUser, + cleanupTestData, createTestPrompt, createTestPromptAnswers, - cleanupTestData, - getUserMatches, + createTestUsers, getNudge, - TestUser, + getUserMatches, } from '../utils/testDataGenerator'; -import { db } from '../../../firebaseAdmin'; jest.setTimeout(120000); @@ -30,6 +30,15 @@ describe('Nudging System Integration Tests', () => { let testUsers: TestUser[] = []; let testPromptId: string; + const setupUsersAndMatches = async () => { + // Create users and matches + testUsers = await createTestUsers(6); + const prompt = await createTestPrompt(); + testPromptId = prompt.promptId; + await createTestPromptAnswers(testUsers, testPromptId); + await generateMatchesForPrompt(testPromptId); + }; + beforeAll(async () => { await cleanupTestData(); }); @@ -50,22 +59,23 @@ describe('Nudging System Integration Tests', () => { describe('Basic Nudging Functionality', () => { test('should create a nudge from user A to user B', async () => { - // Create users and matches - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); // User 0 nudges User 1 (assuming they're matched) - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // First match await createNudge(testUsers[0].netid, userBNetid, testPromptId); // Verify nudge was created - const nudge = await getNudge(testUsers[0].netid, userBNetid, testPromptId); + const nudge = await getNudge( + testUsers[0].netid, + userBNetid, + testPromptId + ); expect(nudge).toBeTruthy(); expect(nudge!.fromNetid).toBe(testUsers[0].netid); expect(nudge!.toNetid).toBe(userBNetid); @@ -74,14 +84,12 @@ describe('Nudging System Integration Tests', () => { }); test('should detect mutual nudge when both users nudge each other', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // User A nudges User B @@ -91,22 +99,28 @@ describe('Nudging System Integration Tests', () => { await createNudge(userBNetid, testUsers[0].netid, testPromptId); // Both nudges should be marked as mutual - const nudgeAtoB = await getNudge(testUsers[0].netid, userBNetid, testPromptId); - const nudgeBtoA = await getNudge(userBNetid, testUsers[0].netid, testPromptId); + const nudgeAtoB = await getNudge( + testUsers[0].netid, + userBNetid, + testPromptId + ); + const nudgeBtoA = await getNudge( + userBNetid, + testUsers[0].netid, + testPromptId + ); expect(nudgeAtoB!.mutual).toBe(true); expect(nudgeBtoA!.mutual).toBe(true); }); test('should unlock chat for both users on mutual nudge', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // Before nudging, chatUnlocked should not exist or be false @@ -119,12 +133,17 @@ describe('Nudging System Integration Tests', () => { await createNudge(userBNetid, testUsers[0].netid, testPromptId); // Get updated match data - const userAMatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBMatchesAfter = await getUserMatches(userBNetid, testPromptId); // Find the index of each user in the other's matches array const indexOfBInA = userAMatchesAfter!.matches.indexOf(userBNetid); - const indexOfAInB = userBMatchesAfter!.matches.indexOf(testUsers[0].netid); + const indexOfAInB = userBMatchesAfter!.matches.indexOf( + testUsers[0].netid + ); // chatUnlocked should be set for the specific match index expect(userAMatchesAfter!.chatUnlocked).toBeDefined(); @@ -135,14 +154,12 @@ describe('Nudging System Integration Tests', () => { }); test('should only unlock chat for the specific match, not all matches', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // First match const userCNetid = userAMatches!.matches[1]; // Second match @@ -151,7 +168,10 @@ describe('Nudging System Integration Tests', () => { await createNudge(userBNetid, testUsers[0].netid, testPromptId); // Get updated matches - const userAMatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const indexOfB = userAMatchesAfter!.matches.indexOf(userBNetid); const indexOfC = userAMatchesAfter!.matches.indexOf(userCNetid); @@ -164,18 +184,20 @@ describe('Nudging System Integration Tests', () => { describe('Nudge Status Queries', () => { test('should correctly report nudge status', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // Initial status - no nudges - let status = await getNudgeStatus(testUsers[0].netid, userBNetid, testPromptId); + let status = await getNudgeStatus( + testUsers[0].netid, + userBNetid, + testPromptId + ); expect(status.sent).toBe(false); expect(status.received).toBe(false); expect(status.mutual).toBe(false); @@ -184,13 +206,21 @@ describe('Nudging System Integration Tests', () => { await createNudge(testUsers[0].netid, userBNetid, testPromptId); // Status from User A's perspective - status = await getNudgeStatus(testUsers[0].netid, userBNetid, testPromptId); + status = await getNudgeStatus( + testUsers[0].netid, + userBNetid, + testPromptId + ); expect(status.sent).toBe(true); expect(status.received).toBe(false); expect(status.mutual).toBe(false); // Status from User B's perspective - status = await getNudgeStatus(userBNetid, testUsers[0].netid, testPromptId); + status = await getNudgeStatus( + userBNetid, + testUsers[0].netid, + testPromptId + ); expect(status.sent).toBe(false); expect(status.received).toBe(true); expect(status.mutual).toBe(false); @@ -199,12 +229,20 @@ describe('Nudging System Integration Tests', () => { await createNudge(userBNetid, testUsers[0].netid, testPromptId); // Both should show mutual - status = await getNudgeStatus(testUsers[0].netid, userBNetid, testPromptId); + status = await getNudgeStatus( + testUsers[0].netid, + userBNetid, + testPromptId + ); expect(status.sent).toBe(true); expect(status.received).toBe(true); expect(status.mutual).toBe(true); - status = await getNudgeStatus(userBNetid, testUsers[0].netid, testPromptId); + status = await getNudgeStatus( + userBNetid, + testUsers[0].netid, + testPromptId + ); expect(status.sent).toBe(true); expect(status.received).toBe(true); expect(status.mutual).toBe(true); @@ -213,56 +251,60 @@ describe('Nudging System Integration Tests', () => { describe('Edge Cases and Error Handling', () => { test('should throw error when nudging the same user twice', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // First nudge should succeed await createNudge(testUsers[0].netid, userBNetid, testPromptId); // Second nudge should fail - await expect(createNudge(testUsers[0].netid, userBNetid, testPromptId)).rejects.toThrow( - 'already nudged' - ); + await expect( + createNudge(testUsers[0].netid, userBNetid, testPromptId) + ).rejects.toThrow('already nudged'); }); test('should handle nudging non-matched users gracefully', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const nonMatchedUser = testUsers.find( - (u) => !userAMatches!.matches.includes(u.netid) && u.netid !== testUsers[0].netid + (u) => + !userAMatches!.matches.includes(u.netid) && + u.netid !== testUsers[0].netid ); // Nudging a non-matched user should still create the nudge // (the system doesn't prevent this, though the UI might) - await createNudge(testUsers[0].netid, nonMatchedUser!.netid, testPromptId); + await createNudge( + testUsers[0].netid, + nonMatchedUser!.netid, + testPromptId + ); - const nudge = await getNudge(testUsers[0].netid, nonMatchedUser!.netid, testPromptId); + const nudge = await getNudge( + testUsers[0].netid, + nonMatchedUser!.netid, + testPromptId + ); expect(nudge).toBeTruthy(); }); test('should handle nudges for users with different number of matches', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); // Get two users who are matched - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // Both users nudge each other @@ -270,11 +312,16 @@ describe('Nudging System Integration Tests', () => { await createNudge(userBNetid, testUsers[0].netid, testPromptId); // Even if users have different numbers of matches, chat should unlock correctly - const userAMatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBMatchesAfter = await getUserMatches(userBNetid, testPromptId); const indexOfBInA = userAMatchesAfter!.matches.indexOf(userBNetid); - const indexOfAInB = userBMatchesAfter!.matches.indexOf(testUsers[0].netid); + const indexOfAInB = userBMatchesAfter!.matches.indexOf( + testUsers[0].netid + ); expect(userAMatchesAfter!.chatUnlocked![indexOfBInA]).toBe(true); expect(userBMatchesAfter!.chatUnlocked![indexOfAInB]).toBe(true); @@ -283,14 +330,12 @@ describe('Nudging System Integration Tests', () => { describe('Notification Creation on Mutual Nudge', () => { test('should create notifications for both users on mutual nudge', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // User A nudges User B @@ -325,14 +370,12 @@ describe('Nudging System Integration Tests', () => { describe('Concurrent Nudging', () => { test('should handle concurrent mutual nudges correctly', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(); - const userAMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBNetid = userAMatches!.matches[0]; // Both users nudge simultaneously @@ -342,18 +385,31 @@ describe('Nudging System Integration Tests', () => { ]); // Both nudges should be marked as mutual - const nudgeAtoB = await getNudge(testUsers[0].netid, userBNetid, testPromptId); - const nudgeBtoA = await getNudge(userBNetid, testUsers[0].netid, testPromptId); + const nudgeAtoB = await getNudge( + testUsers[0].netid, + userBNetid, + testPromptId + ); + const nudgeBtoA = await getNudge( + userBNetid, + testUsers[0].netid, + testPromptId + ); expect(nudgeAtoB!.mutual).toBe(true); expect(nudgeBtoA!.mutual).toBe(true); // Chat should be unlocked for both - const userAMatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); + const userAMatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const userBMatchesAfter = await getUserMatches(userBNetid, testPromptId); const indexOfBInA = userAMatchesAfter!.matches.indexOf(userBNetid); - const indexOfAInB = userBMatchesAfter!.matches.indexOf(testUsers[0].netid); + const indexOfAInB = userBMatchesAfter!.matches.indexOf( + testUsers[0].netid + ); expect(userAMatchesAfter!.chatUnlocked![indexOfBInA]).toBe(true); expect(userBMatchesAfter!.chatUnlocked![indexOfAInB]).toBe(true); @@ -400,8 +456,14 @@ describe('Nudging System Integration Tests', () => { await createNudge(testUsers[1].netid, testUsers[0].netid, testPromptId); // Chat should be unlocked - const user0MatchesAfter = await getUserMatches(testUsers[0].netid, testPromptId); - const user1MatchesAfter = await getUserMatches(testUsers[1].netid, testPromptId); + const user0MatchesAfter = await getUserMatches( + testUsers[0].netid, + testPromptId + ); + const user1MatchesAfter = await getUserMatches( + testUsers[1].netid, + testPromptId + ); expect(user0MatchesAfter!.chatUnlocked![0]).toBe(true); expect(user1MatchesAfter!.chatUnlocked![0]).toBe(true); diff --git a/backend/src/__tests__/integration/pushNotifications.integration.test.ts b/backend/src/__tests__/integration/pushNotifications.integration.test.ts index c65eba67..7d076f03 100644 --- a/backend/src/__tests__/integration/pushNotifications.integration.test.ts +++ b/backend/src/__tests__/integration/pushNotifications.integration.test.ts @@ -39,14 +39,21 @@ describe('Push Notifications Integration Tests', () => { beforeAll(async () => { // Mock Expo methods - (Expo.isExpoPushToken as unknown as jest.Mock) = jest.fn().mockReturnValue(true); - (Expo as jest.MockedClass).mockImplementation(() => ({ - sendPushNotificationsAsync: jest - .fn() - .mockResolvedValue([{ status: 'ok' }]), - chunkPushNotifications: jest.fn().mockImplementation((msgs) => [msgs]), - isExpoPushToken: jest.fn().mockReturnValue(true), - } as any)); + (Expo.isExpoPushToken as unknown as jest.Mock) = jest + .fn() + .mockReturnValue(true); + (Expo as jest.MockedClass).mockImplementation( + () => + ({ + sendPushNotificationsAsync: jest + .fn() + .mockResolvedValue([{ status: 'ok' }]), + chunkPushNotifications: jest + .fn() + .mockImplementation((msgs) => [msgs]), + isExpoPushToken: jest.fn().mockReturnValue(true), + }) as any + ); // Clean up test data for (const user of testUsers) { @@ -389,11 +396,7 @@ describe('Push Notifications Integration Tests', () => { createdAt: new Date(), }); - const result = await sendPushNotification( - tempUser.netid, - 'Test', - 'Test' - ); + const result = await sendPushNotification(tempUser.netid, 'Test', 'Test'); expect(result).toBe(false); diff --git a/backend/src/__tests__/integration/reveal.integration.test.ts b/backend/src/__tests__/integration/reveal.integration.test.ts index f3b3b2d5..271714f7 100644 --- a/backend/src/__tests__/integration/reveal.integration.test.ts +++ b/backend/src/__tests__/integration/reveal.integration.test.ts @@ -10,15 +10,17 @@ * - Edge cases and error handling */ -import { revealMatch } from '../../services/matchingService'; -import { generateMatchesForPrompt } from '../../services/matchingService'; import { - createTestUsers, + generateMatchesForPrompt, + revealMatch, +} from '../../services/matchingService'; +import { + TestUser, + cleanupTestData, createTestPrompt, createTestPromptAnswers, - cleanupTestData, + createTestUsers, getUserMatches, - TestUser, } from '../utils/testDataGenerator'; jest.setTimeout(120000); // Increased timeout for integration tests with cleanup @@ -27,6 +29,15 @@ describe('Reveal System Integration Tests', () => { let testUsers: TestUser[] = []; let testPromptId: string; + const setupUsersAndMatches = async (numUsers: number) => { + testUsers = await createTestUsers(numUsers); + const prompt = await createTestPrompt(); + testPromptId = prompt.promptId; + + await createTestPromptAnswers(testUsers, testPromptId); + await generateMatchesForPrompt(testPromptId); + }; + beforeAll(async () => { await cleanupTestData(); }); @@ -47,12 +58,7 @@ describe('Reveal System Integration Tests', () => { describe('Basic Reveal Functionality', () => { test('should reveal a specific match by index', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(10); // Reveal first match (index 0) await revealMatch(testUsers[0].netid, testPromptId, 0); @@ -68,12 +74,7 @@ describe('Reveal System Integration Tests', () => { }); test('should reveal all matches independently', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(10); // Initially all should be unrevealed let matches = await getUserMatches(testUsers[0].netid, testPromptId); @@ -97,12 +98,7 @@ describe('Reveal System Integration Tests', () => { }); test('should not affect other users when revealing a match', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(6); // User 0 reveals their first match await revealMatch(testUsers[0].netid, testPromptId, 0); @@ -115,12 +111,7 @@ describe('Reveal System Integration Tests', () => { }); test('should allow revealing the same match multiple times (idempotent)', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(6); // Reveal first match await revealMatch(testUsers[0].netid, testPromptId, 0); @@ -136,12 +127,7 @@ describe('Reveal System Integration Tests', () => { describe('Reveal with Different Match Counts', () => { test('should handle revealing matches when user has less than 3 matches', async () => { - testUsers = await createTestUsers(3); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(3); // Each user should have 2 matches (only 3 users total) const matches = await getUserMatches(testUsers[0].netid, testPromptId); @@ -152,38 +138,33 @@ describe('Reveal System Integration Tests', () => { await revealMatch(testUsers[0].netid, testPromptId, i); } - const updatedMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const updatedMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); // All matches should be revealed - expect(updatedMatches!.revealed.every((r: boolean) => r === true)).toBe(true); + expect(updatedMatches!.revealed.every((r: boolean) => r === true)).toBe( + true + ); }); }); describe('Error Handling', () => { test('should throw error for invalid match index (negative)', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(6); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - await expect(revealMatch(testUsers[0].netid, testPromptId, -1)).rejects.toThrow( - 'Match index must be between 0 and 2' - ); + await expect( + revealMatch(testUsers[0].netid, testPromptId, -1) + ).rejects.toThrow('Match index must be between 0 and 2'); }); test('should throw error for invalid match index (too high)', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(6); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - await expect(revealMatch(testUsers[0].netid, testPromptId, 3)).rejects.toThrow( - 'Match index must be between 0 and 2' - ); + await expect( + revealMatch(testUsers[0].netid, testPromptId, 3) + ).rejects.toThrow('Match index must be between 0 and 2'); }); test('should throw error when revealing match for non-existent prompt', async () => { @@ -195,32 +176,22 @@ describe('Reveal System Integration Tests', () => { }); test('should throw error when match index exceeds actual match count', async () => { - testUsers = await createTestUsers(3); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(3); const matches = await getUserMatches(testUsers[0].netid, testPromptId); // Try to reveal index 2 when user only has 2 matches (indices 0 and 1) if (matches!.matches.length < 3) { - await expect(revealMatch(testUsers[0].netid, testPromptId, 2)).rejects.toThrow( - 'Match index out of bounds' - ); + await expect( + revealMatch(testUsers[0].netid, testPromptId, 2) + ).rejects.toThrow('Match index out of bounds'); } }); }); describe('Reveal Independence from Nudging', () => { test('should allow revealing matches regardless of nudge status', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(6); // Reveal a match without any nudging await revealMatch(testUsers[0].netid, testPromptId, 0); @@ -237,14 +208,12 @@ describe('Reveal System Integration Tests', () => { }); test('revealing a match should not unlock chat', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(10); - const initialMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const initialMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const matchCount = initialMatches!.matches.length; // Reveal all matches @@ -259,21 +228,21 @@ describe('Reveal System Integration Tests', () => { // But chat should not be unlocked if (matches!.chatUnlocked) { - expect(matches!.chatUnlocked.every((u: boolean) => u === false)).toBe(true); + expect(matches!.chatUnlocked.every((u: boolean) => u === false)).toBe( + true + ); } }); }); describe('Concurrent Reveal Operations', () => { test('should handle concurrent reveals of different matches', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(10); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - const initialMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const initialMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); expect(initialMatches).toBeTruthy(); const matchCount = initialMatches!.matches.length; @@ -292,12 +261,7 @@ describe('Reveal System Integration Tests', () => { }); test('should handle concurrent reveals of same match (idempotent)', async () => { - testUsers = await createTestUsers(6); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(6); // Reveal same match multiple times concurrently await Promise.all([ @@ -315,14 +279,12 @@ describe('Reveal System Integration Tests', () => { describe('Reveal Order Independence', () => { test('should allow revealing matches in any order', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(10); - const initialMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const initialMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const matchCount = initialMatches!.matches.length; // Only test if user has at least 3 matches @@ -389,17 +351,15 @@ describe('Reveal System Integration Tests', () => { describe('Reveal State Persistence', () => { test('should persist revealed state across queries', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; - - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); + await setupUsersAndMatches(10); // Reveal first match await revealMatch(testUsers[0].netid, testPromptId, 0); - const initialMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const initialMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const matchCount = initialMatches!.matches.length; // Query multiple times - state should persist @@ -414,14 +374,12 @@ describe('Reveal System Integration Tests', () => { }); test('should maintain revealed state after revealing additional matches', async () => { - testUsers = await createTestUsers(10); - const prompt = await createTestPrompt(); - testPromptId = prompt.promptId; + await setupUsersAndMatches(10); - await createTestPromptAnswers(testUsers, testPromptId); - await generateMatchesForPrompt(testPromptId); - - const initialMatches = await getUserMatches(testUsers[0].netid, testPromptId); + const initialMatches = await getUserMatches( + testUsers[0].netid, + testPromptId + ); const matchCount = initialMatches!.matches.length; // Reveal matches one by one, checking state persists diff --git a/backend/src/__tests__/pushNotificationService.test.ts b/backend/src/__tests__/pushNotificationService.test.ts index 98b98476..e561d339 100644 --- a/backend/src/__tests__/pushNotificationService.test.ts +++ b/backend/src/__tests__/pushNotificationService.test.ts @@ -50,8 +50,10 @@ describe('Push Notification Service', () => { let mockDoc: jest.Mock; // Get references to the mocks from the module - const mockSendPushNotificationsAsync = (ExpoSDK as any).__mockSendPushNotificationsAsync; - const mockChunkPushNotifications = (ExpoSDK as any).__mockChunkPushNotifications; + const mockSendPushNotificationsAsync = (ExpoSDK as any) + .__mockSendPushNotificationsAsync; + const mockChunkPushNotifications = (ExpoSDK as any) + .__mockChunkPushNotifications; const mockIsExpoPushToken = (ExpoSDK as any).__mockIsExpoPushToken; beforeEach(() => { @@ -336,7 +338,10 @@ describe('Push Notification Service', () => { docs: [{ data: () => mockUserData }], }); - const result = await checkNotificationPreference('test123', 'newMessages'); + const result = await checkNotificationPreference( + 'test123', + 'newMessages' + ); expect(result).toBe(true); }); @@ -356,7 +361,10 @@ describe('Push Notification Service', () => { docs: [{ data: () => mockUserData }], }); - const result = await checkNotificationPreference('test123', 'newMessages'); + const result = await checkNotificationPreference( + 'test123', + 'newMessages' + ); expect(result).toBe(false); }); @@ -371,7 +379,10 @@ describe('Push Notification Service', () => { docs: [{ data: () => mockUserData }], }); - const result = await checkNotificationPreference('test123', 'newMessages'); + const result = await checkNotificationPreference( + 'test123', + 'newMessages' + ); expect(result).toBe(true); }); @@ -382,7 +393,10 @@ describe('Push Notification Service', () => { docs: [], }); - const result = await checkNotificationPreference('test123', 'newMessages'); + const result = await checkNotificationPreference( + 'test123', + 'newMessages' + ); expect(result).toBe(true); }); diff --git a/backend/src/__tests__/utils/testDataGenerator.ts b/backend/src/__tests__/utils/testDataGenerator.ts index 2b2f7128..4bda3ccf 100644 --- a/backend/src/__tests__/utils/testDataGenerator.ts +++ b/backend/src/__tests__/utils/testDataGenerator.ts @@ -89,10 +89,19 @@ export async function createTestUser( const preferences: PreferencesDoc = { netid, ageRange: overrides?.preferences?.ageRange || { min: 18, max: 25 }, - years: overrides?.preferences?.years || ['Freshman', 'Sophomore', 'Junior', 'Senior'], + years: overrides?.preferences?.years || [ + 'Freshman', + 'Sophomore', + 'Junior', + 'Senior', + ], schools: overrides?.preferences?.schools || [], majors: overrides?.preferences?.majors || [], - genders: overrides?.preferences?.genders || ['male', 'female', 'non-binary'], + genders: overrides?.preferences?.genders || [ + 'male', + 'female', + 'non-binary', + ], createdAt: new Date(), updatedAt: new Date(), ...(overrides?.preferences || {}), @@ -122,16 +131,22 @@ export async function createTestUsers(count: number): Promise { netid, profile: { netid, - gender: (i % 3 === 0 ? 'male' : i % 3 === 1 ? 'female' : 'non-binary') as Gender, - year: (['Freshman', 'Sophomore', 'Junior', 'Senior'][i % 4]) as Year, - school: (i % 2 === 0 ? 'College of Engineering' : 'College of Arts and Sciences') as School, + gender: (i % 3 === 0 + ? 'male' + : i % 3 === 1 + ? 'female' + : 'non-binary') as Gender, + year: ['Freshman', 'Sophomore', 'Junior', 'Senior'][i % 4] as Year, + school: (i % 2 === 0 + ? 'College of Engineering' + : 'College of Arts and Sciences') as School, major: i % 2 === 0 ? ['Computer Science'] : ['Biology', 'Psychology'], interests: i % 3 === 0 ? ['coding', 'gaming', 'music'] : i % 3 === 1 - ? ['reading', 'hiking', 'photography'] - : ['music', 'sports', 'cooking'], + ? ['reading', 'hiking', 'photography'] + : ['music', 'sports', 'cooking'], clubs: i % 2 === 0 ? ['CUAppDev'] : ['Hiking Club'], bio: `Test user ${i}`, firstName: `User${i}`, @@ -142,7 +157,8 @@ export async function createTestUsers(count: number): Promise { } as ProfileDoc, preferences: { netid, - genders: i % 2 === 0 ? ['female', 'non-binary'] : ['male', 'non-binary'], + genders: + i % 2 === 0 ? ['female', 'non-binary'] : ['male', 'non-binary'], ageRange: { min: 18, max: 25 }, years: ['Freshman', 'Sophomore', 'Junior', 'Senior'], schools: [], @@ -167,7 +183,9 @@ export async function createTestPrompt( const id = promptId || generateTestPromptId(); const now = new Date(); const nextFriday = new Date(); - nextFriday.setDate(nextFriday.getDate() + ((5 + 7 - nextFriday.getDay()) % 7 || 7)); + nextFriday.setDate( + nextFriday.getDate() + ((5 + 7 - nextFriday.getDay()) % 7 || 7) + ); nextFriday.setHours(0, 0, 0, 0); const prompt: WeeklyPromptDoc = { @@ -206,12 +224,15 @@ export async function createTestPromptAnswers( for (let i = 0; i < users.length; i++) { const user = users[i]; const docId = `${user.netid}_${promptId}`; - await db.collection('weeklyPromptAnswers').doc(docId).set({ - netid: user.netid, - promptId, - answer: answers[i % answers.length], - createdAt: FieldValue.serverTimestamp(), - }); + await db + .collection('weeklyPromptAnswers') + .doc(docId) + .set({ + netid: user.netid, + promptId, + answer: answers[i % answers.length], + createdAt: FieldValue.serverTimestamp(), + }); } } @@ -380,8 +401,14 @@ export async function getAllTestUsers(): Promise { for (const doc of usersSnapshot.docs) { const userData = doc.data(); - const profileDoc = await db.collection('profiles').doc(userData.netid).get(); - const preferencesDoc = await db.collection('preferences').doc(userData.netid).get(); + const profileDoc = await db + .collection('profiles') + .doc(userData.netid) + .get(); + const preferencesDoc = await db + .collection('preferences') + .doc(userData.netid) + .get(); users.push({ netid: userData.netid, @@ -416,7 +443,11 @@ export async function getUserMatches(netid: string, promptId: string) { /** * Get nudge status between two users */ -export async function getNudge(fromNetid: string, toNetid: string, promptId: string) { +export async function getNudge( + fromNetid: string, + toNetid: string, + promptId: string +) { const nudgeId = `${fromNetid}_${promptId}_${toNetid}`; const doc = await db.collection('nudges').doc(nudgeId).get(); return doc.exists ? doc.data() : null; diff --git a/backend/src/middleware/rateLimiting.ts b/backend/src/middleware/rateLimiting.ts index 5a2a57d8..803348e5 100644 --- a/backend/src/middleware/rateLimiting.ts +++ b/backend/src/middleware/rateLimiting.ts @@ -9,7 +9,9 @@ const ipKeyGenerator = (req: Request): string => { // Use the x-forwarded-for header if behind a proxy (e.g., Heroku) const forwardedFor = req.headers['x-forwarded-for']; if (forwardedFor) { - const ip = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor.split(',')[0]; + const ip = Array.isArray(forwardedFor) + ? forwardedFor[0] + : forwardedFor.split(',')[0]; return ip.trim(); } // Fall back to req.ip or socket address @@ -31,7 +33,9 @@ const skipAdmin = async (req: Request): Promise => { try { const isAdmin = await isUserAdmin(authReq.user.uid); if (isAdmin) { - console.log(`✅ [Rate Limit] Admin ${authReq.user.uid} bypassing rate limit`); + console.log( + `✅ [Rate Limit] Admin ${authReq.user.uid} bypassing rate limit` + ); } return isAdmin; } catch (error) { @@ -67,15 +71,21 @@ const generateIpKey = (req: Request): string => { /** * Base configuration factory for creating rate limiters */ -const createRateLimiter = (config: Partial): ReturnType => { +const createRateLimiter = ( + config: Partial +): ReturnType => { return rateLimit({ standardHeaders: true, // Return rate limit info in `RateLimit-*` headers legacyHeaders: false, // Disable `X-RateLimit-*` headers skip: skipAdmin, // Skip rate limiting for admins handler: (req, res) => { const authReq = req as AuthenticatedRequest; - const identifier = authReq.user?.uid ? `user ${authReq.user.uid}` : `IP ${req.ip}`; - console.warn(`⚠️ [Rate Limit] Rate limit exceeded for ${identifier} on ${req.path}`); + const identifier = authReq.user?.uid + ? `user ${authReq.user.uid}` + : `IP ${req.ip}`; + console.warn( + `⚠️ [Rate Limit] Rate limit exceeded for ${identifier} on ${req.path}` + ); res.status(429).json({ error: 'Too many requests, please try again later.', diff --git a/backend/src/routes/__tests__/pushTokens.test.ts b/backend/src/routes/__tests__/pushTokens.test.ts index 2382c9db..009aea17 100644 --- a/backend/src/routes/__tests__/pushTokens.test.ts +++ b/backend/src/routes/__tests__/pushTokens.test.ts @@ -1,7 +1,7 @@ -import request from 'supertest'; import express from 'express'; -import pushTokensRouter from '../pushTokens'; +import request from 'supertest'; import * as deviceTokenService from '../../services/deviceTokenService'; +import pushTokensRouter from '../pushTokens'; // Mock the services jest.mock('../../services/deviceTokenService'); @@ -49,6 +49,23 @@ const mockUserData = { firebaseUid: 'test-firebase-uid-123', }; +type MockDoc = { data: () => typeof mockUserData }; + +const mockGetFunc = (isEmpty: boolean, docs: MockDoc[]) => { + const mockGet = jest.fn().mockResolvedValue({ + empty: isEmpty, + docs: docs, + }); + + app.locals.db.collection = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + limit: jest.fn().mockReturnValue({ + get: mockGet, + }), + }), + }); +}; + describe('Push Tokens Routes', () => { beforeEach(() => { jest.clearAllMocks(); @@ -57,19 +74,7 @@ describe('Push Tokens Routes', () => { describe('POST /api/users/push-token', () => { it('should register push token successfully', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: false, - docs: [{ data: () => mockUserData }], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(false, [{ data: () => mockUserData }]); (deviceTokenService.registerPushToken as jest.Mock).mockResolvedValue( true ); @@ -102,19 +107,7 @@ describe('Push Tokens Routes', () => { }); it('should return 400 if pushToken is invalid format', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: false, - docs: [{ data: () => mockUserData }], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(false, [{ data: () => mockUserData }]); (deviceTokenService.registerPushToken as jest.Mock).mockRejectedValue( new Error('Invalid push token format') ); @@ -125,23 +118,14 @@ describe('Push Tokens Routes', () => { .send({ pushToken: 'invalid-token' }) .expect(400); - expect(response.body).toHaveProperty('error', 'Invalid push token format'); + expect(response.body).toHaveProperty( + 'error', + 'Invalid push token format' + ); }); it('should return 404 if user not found', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: true, - docs: [], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(true, []); const response = await request(app) .post('/api/users/push-token') .set('Authorization', 'Bearer valid-token') @@ -163,19 +147,7 @@ describe('Push Tokens Routes', () => { describe('DELETE /api/users/push-token', () => { it('should remove push token successfully', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: false, - docs: [{ data: () => mockUserData }], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(false, [{ data: () => mockUserData }]); (deviceTokenService.removePushToken as jest.Mock).mockResolvedValue(true); const response = await request(app) @@ -188,23 +160,13 @@ describe('Push Tokens Routes', () => { message: 'Push token removed successfully', }); - expect(deviceTokenService.removePushToken).toHaveBeenCalledWith('test123'); + expect(deviceTokenService.removePushToken).toHaveBeenCalledWith( + 'test123' + ); }); it('should return 404 if user not found', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: true, - docs: [], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(true, []); const response = await request(app) .delete('/api/users/push-token') .set('Authorization', 'Bearer valid-token') @@ -224,19 +186,7 @@ describe('Push Tokens Routes', () => { describe('GET /api/users/notification-preferences', () => { it('should return notification preferences', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: false, - docs: [{ data: () => mockUserData }], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(false, [{ data: () => mockUserData }]); const mockPreferences = { newMessages: true, matchDrops: false, @@ -259,19 +209,7 @@ describe('Push Tokens Routes', () => { }); it('should return 404 if user not found', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: true, - docs: [], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(true, []); const response = await request(app) .get('/api/users/notification-preferences') .set('Authorization', 'Bearer valid-token') @@ -291,19 +229,7 @@ describe('Push Tokens Routes', () => { describe('PUT /api/users/notification-preferences', () => { it('should update notification preferences successfully', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: false, - docs: [{ data: () => mockUserData }], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(false, [{ data: () => mockUserData }]); const updatedPreferences = { newMessages: false, matchDrops: true, @@ -335,19 +261,7 @@ describe('Push Tokens Routes', () => { }); it('should update multiple preferences at once', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: false, - docs: [{ data: () => mockUserData }], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(false, [{ data: () => mockUserData }]); const updatedPreferences = { newMessages: false, matchDrops: false, @@ -402,19 +316,7 @@ describe('Push Tokens Routes', () => { }); it('should return 404 if user not found', async () => { - const mockGet = jest.fn().mockResolvedValue({ - empty: true, - docs: [], - }); - - app.locals.db.collection = jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: mockGet, - }), - }), - }); - + mockGetFunc(true, []); const response = await request(app) .put('/api/users/notification-preferences') .set('Authorization', 'Bearer valid-token') diff --git a/backend/src/routes/admin-matches.ts b/backend/src/routes/admin-matches.ts index 8f42d340..bd7b9027 100644 --- a/backend/src/routes/admin-matches.ts +++ b/backend/src/routes/admin-matches.ts @@ -6,7 +6,6 @@ */ import express from 'express'; -import { FieldValue, Timestamp } from 'firebase-admin/firestore'; import { db } from '../../firebaseAdmin'; import { ProfileDoc } from '../../types'; import { AdminRequest, requireAdmin } from '../middleware/adminAuth'; @@ -78,80 +77,91 @@ interface UserDetailsResponse { * @returns {Error} 404 - User not found * @returns {Error} 500 - Internal server error */ -router.get('/api/admin/users/:netId/details', async (req: AdminRequest, res) => { - try { - const { netId } = req.params; - const { promptId } = req.query; - - console.log(`Admin ${req.user?.email} fetching details for user ${netId}`); - - // Fetch profile by querying netid field (profiles use auto-generated IDs) - const profileSnapshot = await db - .collection('profiles') - .where('netid', '==', netId) - .limit(1) - .get(); - - if (profileSnapshot.empty) { - return res.status(404).json({ error: 'User profile not found' }); - } +router.get( + '/api/admin/users/:netId/details', + async (req: AdminRequest, res) => { + try { + const { netId } = req.params; + const { promptId } = req.query; - const profileData = profileSnapshot.docs[0].data() as ProfileDoc; + console.log( + `Admin ${req.user?.email} fetching details for user ${netId}` + ); - // Fetch prompt answer if promptId provided - let promptAnswer = null; - if (promptId && typeof promptId === 'string' && promptId.trim() !== '' && netId.trim() !== '') { - const answerDoc = await db - .collection('weeklyPromptAnswers') - .doc(`${netId}_${promptId}`) + // Fetch profile by querying netid field (profiles use auto-generated IDs) + const profileSnapshot = await db + .collection('profiles') + .where('netid', '==', netId) + .limit(1) .get(); - if (answerDoc.exists) { - promptAnswer = answerDoc.data()?.answer || null; + if (profileSnapshot.empty) { + return res.status(404).json({ error: 'User profile not found' }); } - } - const userDetails: UserDetailsResponse = { - netId, - firstName: profileData.firstName || 'Unknown', - pictures: profileData.pictures || [], - promptAnswer, - }; + const profileData = profileSnapshot.docs[0].data() as ProfileDoc; + + // Fetch prompt answer if promptId provided + let promptAnswer = null; + if ( + promptId && + typeof promptId === 'string' && + promptId.trim() !== '' && + netId.trim() !== '' + ) { + const answerDoc = await db + .collection('weeklyPromptAnswers') + .doc(`${netId}_${promptId}`) + .get(); + + if (answerDoc.exists) { + promptAnswer = answerDoc.data()?.answer || null; + } + } - // Log successful action - await logAdminAction( - 'VIEW_USER', - req.user!.uid, - req.user!.email, - 'user', - netId, - { promptId: promptId || null }, - getIpAddress(req), - getUserAgent(req) - ); + const userDetails: UserDetailsResponse = { + netId, + firstName: profileData.firstName || 'Unknown', + pictures: profileData.pictures || [], + promptAnswer, + }; - res.status(200).json(userDetails); - } catch (error) { - console.error('Error fetching user details:', error); - const errorMessage = error instanceof Error ? error.message : String(error); + // Log successful action + await logAdminAction( + 'VIEW_USER', + req.user!.uid, + req.user!.email, + 'user', + netId, + { promptId: promptId || null }, + getIpAddress(req), + getUserAgent(req) + ); - // Log failed action - await logAdminAction( - 'VIEW_USER', - req.user!.uid, - req.user!.email, - 'user', - req.params.netId, - { error: errorMessage }, - getIpAddress(req), - getUserAgent(req), - false, - errorMessage - ); + res.status(200).json(userDetails); + } catch (error) { + console.error('Error fetching user details:', error); + const errorMessage = + error instanceof Error ? error.message : String(error); + + // Log failed action + await logAdminAction( + 'VIEW_USER', + req.user!.uid, + req.user!.email, + 'user', + req.params.netId, + { error: errorMessage }, + getIpAddress(req), + getUserAgent(req), + false, + errorMessage + ); - res.status(500).json({ error: errorMessage }); + res.status(500).json({ error: errorMessage }); + } } -}); +); /** * GET /api/admin/users @@ -257,10 +267,7 @@ router.post('/api/admin/matches/manual', async (req: AdminRequest, res) => { try { const matchData: CreateManualMatchInput = req.body; - console.log( - `Admin ${req.user?.email} creating manual match:`, - matchData - ); + console.log(`Admin ${req.user?.email} creating manual match`); // Validate required fields if ( @@ -343,7 +350,9 @@ router.post('/api/admin/matches/manual', async (req: AdminRequest, res) => { } catch (createError) { // Handle match creation errors (e.g., matches already exist and append not enabled) const errorMsg = - createError instanceof Error ? createError.message : String(createError); + createError instanceof Error + ? createError.message + : String(createError); if (errorMsg.includes('already exist')) { return res.status(409).json({ diff --git a/backend/src/routes/admin-prompts.ts b/backend/src/routes/admin-prompts.ts index b86a317e..5d91a711 100644 --- a/backend/src/routes/admin-prompts.ts +++ b/backend/src/routes/admin-prompts.ts @@ -837,16 +837,17 @@ router.get('/api/admin/matches/stats', async (req: AdminRequest, res) => { promptMatchCounts[promptId].count++; promptMatchCounts[promptId].nudges += nudgesForThisMatch; } else { - console.warn(`Match document ${matchDoc.id} has invalid promptId:`, promptId); + console.warn( + `Match document ${matchDoc.id} has invalid promptId:`, + promptId + ); } } const totalUsersMatched = uniqueUsers.size; const totalPossibleNudges = totalMatches * 3; // Each match has 3 potential nudges const nudgeRate = - totalPossibleNudges > 0 - ? (totalNudges / totalPossibleNudges) * 100 - : 0; + totalPossibleNudges > 0 ? (totalNudges / totalPossibleNudges) * 100 : 0; const averageMatchesPerPrompt = Object.keys(promptMatchCounts).length > 0 ? totalMatches / Object.keys(promptMatchCounts).length @@ -1016,8 +1017,15 @@ router.get( : []; // Skip if userNetid is invalid - if (!userNetid || typeof userNetid !== 'string' || userNetid.trim() === '') { - console.warn(`Invalid userNetid for match document ${matchDoc.id}:`, userNetid); + if ( + !userNetid || + typeof userNetid !== 'string' || + userNetid.trim() === '' + ) { + console.warn( + `Invalid userNetid for match document ${matchDoc.id}:`, + userNetid + ); continue; // Skip this entire match document } @@ -1038,8 +1046,15 @@ router.get( const matchedNetid = matchedNetids[i]; // Skip if matchedNetid is empty, null, or undefined - if (!matchedNetid || typeof matchedNetid !== 'string' || matchedNetid.trim() === '') { - console.warn(`Invalid matchedNetid at index ${i} for user ${userNetid}:`, matchedNetid); + if ( + !matchedNetid || + typeof matchedNetid !== 'string' || + matchedNetid.trim() === '' + ) { + console.warn( + `Invalid matchedNetid at index ${i} for user ${userNetid}:`, + matchedNetid + ); matchedProfiles.push({ netid: matchedNetid || 'invalid', firstName: 'Invalid User', @@ -1063,8 +1078,10 @@ router.get( : null; // Check nudge status in both directions - const nudgedByUser = nudgeMap.get(userNetid)?.has(matchedNetid) || false; - const nudgedByMatch = nudgeMap.get(matchedNetid)?.has(userNetid) || false; + const nudgedByUser = + nudgeMap.get(userNetid)?.has(matchedNetid) || false; + const nudgedByMatch = + nudgeMap.get(matchedNetid)?.has(userNetid) || false; // Count total nudges (count each direction once) if (nudgedByUser) { @@ -1097,9 +1114,7 @@ router.get( const totalPossibleNudges = totalMatchDocuments * 3; const nudgeRate = - totalPossibleNudges > 0 - ? (totalNudges / totalPossibleNudges) * 100 - : 0; + totalPossibleNudges > 0 ? (totalNudges / totalPossibleNudges) * 100 : 0; const response: PromptMatchDetailResponse = { promptId, diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 75459b6a..f3a24438 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -21,7 +21,8 @@ router.post( try { // Get redirect URL from environment variable or use production default // For local development, set WEB_REDIRECT_URL=http://localhost:3000 - const webRedirectUrl = process.env.WEB_REDIRECT_URL || 'https://redi.love'; + const webRedirectUrl = + process.env.WEB_REDIRECT_URL || 'https://redi.love'; // Generate Firebase sign-in link using Admin SDK (without sending email) const actionCodeSettings = { @@ -36,10 +37,9 @@ router.post( }, }; - const signInLink = await admin.auth().generateSignInWithEmailLink( - email, - actionCodeSettings - ); + const signInLink = await admin + .auth() + .generateSignInWithEmailLink(email, actionCodeSettings); // Send the email using Nodemailer await sendEmail({ diff --git a/backend/src/routes/geocode.ts b/backend/src/routes/geocode.ts index 6d0ad44f..3c724b59 100644 --- a/backend/src/routes/geocode.ts +++ b/backend/src/routes/geocode.ts @@ -11,20 +11,22 @@ router.get( async (req: AuthenticatedRequest, res) => { const q = req.query.q as string; - if (!q || q.trim().length < 3) { - return res.status(400).json({ error: 'Query must be at least 3 characters' }); + return res + .status(400) + .json({ error: 'Query must be at least 3 characters' }); } const apiKey = process.env.GEOAPIFY_API_KEY; if (!apiKey) { - return res.status(500).json({ error: 'Geocoding service not configured' }); + return res + .status(500) + .json({ error: 'Geocoding service not configured' }); } try { const url = `https://api.geoapify.com/v1/geocode/autocomplete?text=${encodeURIComponent(q.trim())}&type=city&limit=5&apiKey=${apiKey}`; const response = await fetch(url); - console.log("test") if (!response.ok) { return res.status(502).json({ error: 'Geocoding service unavailable' }); diff --git a/backend/src/routes/landing-page.ts b/backend/src/routes/landing-page.ts index fc1edc9f..4f9f9549 100644 --- a/backend/src/routes/landing-page.ts +++ b/backend/src/routes/landing-page.ts @@ -8,23 +8,27 @@ import { validateBulkEmailUpload, validate } from '../middleware/validation'; const router = express.Router(); // GET the number of users signed up on the wait list (public endpoint) -router.get('/api/registered-count', publicRateLimit, async (req: Request, res: Response) => { - try { - const doc = db.collection('stats').doc('global'); - const snapshot = await doc.get(); +router.get( + '/api/registered-count', + publicRateLimit, + async (req: Request, res: Response) => { + try { + const doc = db.collection('stats').doc('global'); + const snapshot = await doc.get(); - if (!snapshot.exists) { - // Create the stats document with initial count if it doesn't exist - await doc.set({ userCount: 0 }); - return res.status(200).json({ userCount: 0 }); - } + if (!snapshot.exists) { + // Create the stats document with initial count if it doesn't exist + await doc.set({ userCount: 0 }); + return res.status(200).json({ userCount: 0 }); + } - res.status(200).json(snapshot.data()); - } catch (error) { - console.error('Error fetching user count:', error); - res.status(500).json({ error: 'Failed to fetch user count' }); + res.status(200).json(snapshot.data()); + } catch (error) { + console.error('Error fetching user count:', error); + res.status(500).json({ error: 'Failed to fetch user count' }); + } } -}); +); // POST bulk upload emails (admin only, requires authentication) router.post( diff --git a/backend/src/routes/profiles.ts b/backend/src/routes/profiles.ts index 49b1cf2e..dcb101e4 100644 --- a/backend/src/routes/profiles.ts +++ b/backend/src/routes/profiles.ts @@ -287,7 +287,10 @@ router.get( } // Parse comma-separated UIDs - const uidList = uids.split(',').map((uid) => uid.trim()).filter(Boolean); + const uidList = uids + .split(',') + .map((uid) => uid.trim()) + .filter(Boolean); if (uidList.length === 0) { return res.status(400).json({ @@ -302,11 +305,14 @@ router.get( } // Create result map - const profilesMap: Record = {}; + const profilesMap: Record< + string, + { + firstName: string; + pictures: string[]; + netid: string; + } + > = {}; // Fetch user documents to get netids from Firebase UIDs const userPromises = uidList.map(async (firebaseUid) => { @@ -344,7 +350,10 @@ router.get( pictures: profileData.pictures || [], }; } catch (error) { - console.error(`Error fetching profile for UID ${firebaseUid}:`, error); + console.error( + `Error fetching profile for UID ${firebaseUid}:`, + error + ); return null; } }); diff --git a/backend/src/routes/pushTokens.ts b/backend/src/routes/pushTokens.ts index 95554a09..c292fef6 100644 --- a/backend/src/routes/pushTokens.ts +++ b/backend/src/routes/pushTokens.ts @@ -206,8 +206,7 @@ router.put( mutualNudges === undefined ) { return res.status(400).json({ - error: - 'At least one notification preference must be provided', + error: 'At least one notification preference must be provided', }); } diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts index 359c7f68..0c30ca58 100644 --- a/backend/src/routes/users.ts +++ b/backend/src/routes/users.ts @@ -5,9 +5,7 @@ import { bucket, db } from '../../firebaseAdmin'; import { FirestoreDoc, UserDoc, UserDocWrite, UserResponse } from '../../types'; import { AdminRequest, requireAdmin } from '../middleware/adminAuth'; import { AuthenticatedRequest, authenticateUser } from '../middleware/auth'; -import { - authenticationRateLimit -} from '../middleware/rateLimiting'; +import { authenticationRateLimit } from '../middleware/rateLimiting'; import { validate, validateUserCreation } from '../middleware/validation'; const router = express.Router(); @@ -34,18 +32,22 @@ const userDocToResponse = (doc: FirestoreDoc): UserResponse => ({ }); // GET all users (admin-only endpoint) -router.get('/api/users', requireAdmin, async (req: AdminRequest, res: Response) => { - try { - const snapshot = await db.collection('users').get(); - const users: UserResponse[] = snapshot.docs.map((doc) => - userDocToResponse({ id: doc.id, ...(doc.data() as UserDoc) }) - ); - res.status(200).json(users); - } catch (error) { - console.error('Error fetching users:', error); - res.status(500).json({ error: 'Failed to fetch users' }); +router.get( + '/api/users', + requireAdmin, + async (req: AdminRequest, res: Response) => { + try { + const snapshot = await db.collection('users').get(); + const users: UserResponse[] = snapshot.docs.map((doc) => + userDocToResponse({ id: doc.id, ...(doc.data() as UserDoc) }) + ); + res.status(200).json(users); + } catch (error) { + console.error('Error fetching users:', error); + res.status(500).json({ error: 'Failed to fetch users' }); + } } -}); +); // GET user by netid (admin-only or own user) router.get( @@ -129,13 +131,17 @@ router.post( } // If user exists, return their info - console.log('🆕 [firebase-create] Checking if user already exists:', { netid }); + console.log('🆕 [firebase-create] Checking if user already exists:', { + netid, + }); const existingUser = await db .collection('users') .where('netid', '==', netid) .get(); if (!existingUser.empty) { - console.log('ℹ️ [firebase-create] User already exists, returning existing user'); + console.log( + 'ℹ️ [firebase-create] User already exists, returning existing user' + ); const doc = existingUser.docs[0]; const user = userDocToResponse({ id: doc.id, @@ -160,7 +166,11 @@ router.post( }; const docRef = await db.collection('users').add(userDoc); - console.log('✅ [firebase-create] User created successfully:', { docId: docRef.id, netid, email }); + console.log('✅ [firebase-create] User created successfully:', { + docId: docRef.id, + netid, + email, + }); res.status(201).json({ id: docRef.id, netid, @@ -185,7 +195,10 @@ router.post( // Use authenticated user's email and uid from verified token const email = req.user!.email; const firebaseUid = req.user!.uid; - console.log('🔑 [firebase-login] User from token:', { email, firebaseUid }); + console.log('🔑 [firebase-login] User from token:', { + email, + firebaseUid, + }); if (!email || !firebaseUid) { return res @@ -201,14 +214,22 @@ router.post( .json({ error: 'Only Cornell emails (@cornell.edu) are allowed' }); } - console.log('🔑 [firebase-login] Querying users collection for:', { netid, firebaseUid }); + console.log('🔑 [firebase-login] Querying users collection for:', { + netid, + firebaseUid, + }); const snapshot = await db .collection('users') .where('netid', '==', netid) .where('firebaseUid', '==', firebaseUid) .get(); - console.log('🔑 [firebase-login] Query result - empty:', snapshot.empty, 'size:', snapshot.size); + console.log( + '🔑 [firebase-login] Query result - empty:', + snapshot.empty, + 'size:', + snapshot.size + ); if (snapshot.empty) { console.log('❌ [firebase-login] User not found, returning 401'); @@ -293,7 +314,10 @@ router.delete( await admin.auth().deleteUser(firebaseUid); console.log(`Deleted Firebase Auth user: ${firebaseUid}`); } catch (error) { - console.error(`Error deleting Firebase Auth user ${firebaseUid}:`, error); + console.error( + `Error deleting Firebase Auth user ${firebaseUid}:`, + error + ); throw new Error('Failed to delete Firebase Authentication user'); } @@ -404,7 +428,9 @@ router.delete( batch.delete(doc.ref); }); await batch.commit(); - console.log(`Deleted ${notificationsSnapshot.size} notification documents`); + console.log( + `Deleted ${notificationsSnapshot.size} notification documents` + ); const blockedByUserSnapshot = await db .collection('blockedUsers') diff --git a/backend/src/services/__tests__/matchingAlgorithm.test.ts b/backend/src/services/__tests__/matchingAlgorithm.test.ts index cb5cc9e8..2e295505 100644 --- a/backend/src/services/__tests__/matchingAlgorithm.test.ts +++ b/backend/src/services/__tests__/matchingAlgorithm.test.ts @@ -3,13 +3,13 @@ * Tests the core compatibility checking logic, especially majors and schools exclusion */ +import { ALL_MAJORS, CORNELL_SCHOOLS } from '../../../../constants/cornell'; +import { PreferencesDoc, ProfileDoc } from '../../../types'; import { + calculateCompatibilityScore, checkCompatibility, checkMutualCompatibility, - calculateCompatibilityScore, } from '../matchingAlgorithm'; -import { PreferencesDoc, ProfileDoc } from '../../../types'; -import { ALL_MAJORS, CORNELL_SCHOOLS } from '../../../constants/cornell'; // Helper function to create a basic profile for testing function createTestProfile(overrides: Partial = {}): ProfileDoc { @@ -252,9 +252,7 @@ describe('Matching Algorithm - checkCompatibility', () => { majors: [], }); - expect(checkCompatibility(profile, preferencesExcludeSchool)).toBe( - false - ); + expect(checkCompatibility(profile, preferencesExcludeSchool)).toBe(false); // Preferences that exclude this profile's major const preferencesExcludeMajor = createTestPreferences({ @@ -316,7 +314,7 @@ describe('Matching Algorithm - checkMutualCompatibility', () => { ).toBe(true); }); - test('Should fail if user A excludes user B\'s major', () => { + test("Should fail if user A excludes user B's major", () => { const profileA = createTestProfile({ netid: 'user1', gender: 'female', @@ -347,7 +345,7 @@ describe('Matching Algorithm - checkMutualCompatibility', () => { ).toBe(false); }); - test('Should fail if user B excludes user A\'s school', () => { + test("Should fail if user B excludes user A's school", () => { const profileA = createTestProfile({ netid: 'user1', gender: 'female', diff --git a/backend/src/services/__tests__/matchingService.simple.test.ts b/backend/src/services/__tests__/matchingService.simple.test.ts index 48e9cbf0..c0b14429 100644 --- a/backend/src/services/__tests__/matchingService.simple.test.ts +++ b/backend/src/services/__tests__/matchingService.simple.test.ts @@ -3,16 +3,25 @@ * Tests core logic without full Firestore mocking complexity */ -import { - validateMatchMutuality, -} from '../matchingService'; import { db } from '../../../firebaseAdmin'; +import { validateMatchMutuality } from '../matchingService'; // Mock Firebase jest.mock('../../../firebaseAdmin'); const mockDb = db as jest.Mocked; +const setupMockCollection = (docs: { data: () => object }[]) => { + const mockGet = jest.fn().mockResolvedValue({ + forEach: (callback: any) => docs.forEach((doc) => callback(doc)), + }); + const mockWhere = jest.fn().mockReturnValue({ get: mockGet }); + const mockCollection = jest.fn().mockReturnValue({ + where: mockWhere, + }); + mockDb.collection = mockCollection as any; +}; + describe('Two-Phase Mutual Matching - Core Logic Tests', () => { describe('validateMatchMutuality', () => { beforeEach(() => { @@ -49,20 +58,7 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { }), }, ]; - - const mockGet = jest.fn().mockResolvedValue({ - forEach: (callback: any) => mockDocs.forEach(callback), - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection(mockDocs); const result = await validateMatchMutuality(promptId); @@ -85,25 +81,15 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { }, ]; - const mockGet = jest.fn().mockResolvedValue({ - forEach: (callback: any) => mockDocs.forEach(callback), - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection(mockDocs); const result = await validateMatchMutuality(promptId); expect(result.isValid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); - expect(result.errors.some((e) => e.includes('invalid match values'))).toBe(true); + expect( + result.errors.some((e) => e.includes('invalid match values')) + ).toBe(true); }); test('should pass validation for all mutual matches', async () => { @@ -136,19 +122,7 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { }, ]; - const mockGet = jest.fn().mockResolvedValue({ - forEach: (callback: any) => mockDocs.forEach(callback), - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection(mockDocs); const result = await validateMatchMutuality(promptId); @@ -209,19 +183,7 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { }, ]; - const mockGet = jest.fn().mockResolvedValue({ - forEach: (callback: any) => mockDocs.forEach(callback), - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection(mockDocs); const result = await validateMatchMutuality(promptId); @@ -243,24 +205,14 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { }, ]; - const mockGet = jest.fn().mockResolvedValue({ - forEach: (callback: any) => mockDocs.forEach(callback), - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection(mockDocs); const result = await validateMatchMutuality(promptId); expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('invalid match values'))).toBe(true); + expect( + result.errors.some((e) => e.includes('invalid match values')) + ).toBe(true); }); test('should detect whitespace-only strings', async () => { @@ -277,42 +229,20 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { }, ]; - const mockGet = jest.fn().mockResolvedValue({ - forEach: (callback: any) => mockDocs.forEach(callback), - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection(mockDocs); const result = await validateMatchMutuality(promptId); expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('invalid match values'))).toBe(true); + expect( + result.errors.some((e) => e.includes('invalid match values')) + ).toBe(true); }); test('should handle empty matches collection', async () => { const promptId = 'test-prompt-7'; - const mockGet = jest.fn().mockResolvedValue({ - forEach: jest.fn(), // No docs - }); - - const mockWhere = jest.fn().mockReturnValue({ - get: mockGet, - }); - - const mockCollection = jest.fn().mockReturnValue({ - where: mockWhere, - }); - - mockDb.collection = mockCollection as any; + setupMockCollection([]); const result = await validateMatchMutuality(promptId); @@ -377,9 +307,13 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { // userB ↔ userD (mutual) // userC ↔ userD (mutual) expect(finalMatches.get('userA')).toEqual(['userB']); - expect(finalMatches.get('userB')).toEqual(expect.arrayContaining(['userA', 'userD'])); + expect(finalMatches.get('userB')).toEqual( + expect.arrayContaining(['userA', 'userD']) + ); expect(finalMatches.get('userC')).toEqual(['userD']); - expect(finalMatches.get('userD')).toEqual(expect.arrayContaining(['userB', 'userC'])); + expect(finalMatches.get('userD')).toEqual( + expect.arrayContaining(['userB', 'userC']) + ); }); test('filtering guarantees: empty strings, nulls, and self-references are removed', () => { @@ -403,7 +337,13 @@ describe('Two-Phase Mutual Matching - Core Logic Tests', () => { test('match limit: users get maximum of 3 matches', () => { const potentialMatches = new Map(); - potentialMatches.set('userA', ['userB', 'userC', 'userD', 'userE', 'userF']); + potentialMatches.set('userA', [ + 'userB', + 'userC', + 'userD', + 'userE', + 'userF', + ]); potentialMatches.set('userB', ['userA']); potentialMatches.set('userC', ['userA']); potentialMatches.set('userD', ['userA']); diff --git a/backend/src/services/analyticsService.ts b/backend/src/services/analyticsService.ts index f39bf30a..6590289c 100644 --- a/backend/src/services/analyticsService.ts +++ b/backend/src/services/analyticsService.ts @@ -252,7 +252,9 @@ export async function getCompatibilityMatrix(): Promise { const [userDemo, targetDemo] = key.split(':') as [ DemographicCategory, - DemographicCategory + DemographicCategory, ]; return { userDemographic: userDemo, @@ -449,7 +451,10 @@ export async function getMutualNudgeStats(): Promise { const preferences = preferencesMap.get(userNetid); const demographic = categorizeDemographic(profile.gender, preferences); - const stats = demographicStats.get(demographic) || { matches: 0, mutual: 0 }; + const stats = demographicStats.get(demographic) || { + matches: 0, + mutual: 0, + }; // Count each match (match.matches || []).forEach((matchedNetid: string) => { diff --git a/backend/src/services/matchingAlgorithm.ts b/backend/src/services/matchingAlgorithm.ts index bc3cd13c..7e64f7fd 100644 --- a/backend/src/services/matchingAlgorithm.ts +++ b/backend/src/services/matchingAlgorithm.ts @@ -9,8 +9,8 @@ * - /backend/functions/src/types.ts when in /backend/functions/src/services/ (copied during build) */ +import { ALL_MAJORS, CORNELL_SCHOOLS } from '../../../constants/cornell'; import { PreferencesDoc, ProfileDoc, Year } from '../../types'; -import { ALL_MAJORS, CORNELL_SCHOOLS } from '../../constants/cornell'; /** * UserData interface for matching algorithm @@ -101,8 +101,16 @@ export function calculateMutualCompatibilityScore( relaxed: boolean = false ): number { // Calculate individual compatibility scores (0-100 each) - const aLikesBScore = calculatePreferenceMatchScore(profileB, preferencesA, relaxed); - const bLikesAScore = calculatePreferenceMatchScore(profileA, preferencesB, relaxed); + const aLikesBScore = calculatePreferenceMatchScore( + profileB, + preferencesA, + relaxed + ); + const bLikesAScore = calculatePreferenceMatchScore( + profileA, + preferencesB, + relaxed + ); // If either user has 0 score (fails hard requirements like gender), return 0 if (aLikesBScore === 0 || bLikesAScore === 0) { diff --git a/backend/src/services/matchingService.ts b/backend/src/services/matchingService.ts index 4229ff53..2888ec93 100644 --- a/backend/src/services/matchingService.ts +++ b/backend/src/services/matchingService.ts @@ -300,7 +300,9 @@ export async function generateMatchesForPrompt( promptId: string ): Promise { console.log(`\n${'='.repeat(60)}`); - console.log(`Starting TWO-PHASE MUTUAL match generation for prompt: ${promptId}`); + console.log( + `Starting TWO-PHASE MUTUAL match generation for prompt: ${promptId}` + ); console.log(`${'='.repeat(60)}\n`); // Get all users who answered the prompt @@ -320,7 +322,9 @@ export async function generateMatchesForPrompt( // Get blocked users map (bidirectional blocking) const blockedUsersMap = await getBlockedUsersMap(userNetids); - console.log(`🚫 Fetched blocking relationships for ${userNetids.length} users\n`); + console.log( + `🚫 Fetched blocking relationships for ${userNetids.length} users\n` + ); // ============================================================================= // PHASE 1: Calculate potential matches for all users @@ -363,7 +367,9 @@ export async function generateMatchesForPrompt( potentialMatches.set(netid, validMatches); if (validMatches.length > 0) { - console.log(` ${netid}: ${validMatches.length} potential matches → [${validMatches.join(', ')}]`); + console.log( + ` ${netid}: ${validMatches.length} potential matches → [${validMatches.join(', ')}]` + ); } else { console.log(` ${netid}: 0 potential matches`); } @@ -373,13 +379,17 @@ export async function generateMatchesForPrompt( } } - console.log(`\n✓ Phase 1 complete. Processed ${userNetids.length - usersSkipped} users, skipped ${usersSkipped}\n`); + console.log( + `\n✓ Phase 1 complete. Processed ${userNetids.length - usersSkipped} users, skipped ${usersSkipped}\n` + ); // ============================================================================= // PHASE 1.5: Retry with relaxed criteria for users with 0 matches // ============================================================================= console.log(`${'='.repeat(60)}`); - console.log('PHASE 1.5: Retrying with relaxed criteria for users with 0 matches...'); + console.log( + 'PHASE 1.5: Retrying with relaxed criteria for users with 0 matches...' + ); console.log(`${'='.repeat(60)}\n`); let usersRetried = 0; @@ -420,9 +430,13 @@ export async function generateMatchesForPrompt( if (validMatches.length > 0) { potentialMatches.set(netid, validMatches); usersFoundMatchesRelaxed++; - console.log(` ♻️ ${netid}: Found ${validMatches.length} relaxed matches → [${validMatches.join(', ')}]`); + console.log( + ` ♻️ ${netid}: Found ${validMatches.length} relaxed matches → [${validMatches.join(', ')}]` + ); } else { - console.log(` ⚠️ ${netid}: Still 0 matches even with relaxed criteria`); + console.log( + ` ⚠️ ${netid}: Still 0 matches even with relaxed criteria` + ); } } catch (error) { console.error(`❌ Error finding relaxed matches for ${netid}:`, error); @@ -432,7 +446,9 @@ export async function generateMatchesForPrompt( console.log(`\n✓ Phase 1.5 complete.`); console.log(` Users retried with relaxed criteria: ${usersRetried}`); - console.log(` Users who found matches via relaxed mode: ${usersFoundMatchesRelaxed}\n`); + console.log( + ` Users who found matches via relaxed mode: ${usersFoundMatchesRelaxed}\n` + ); // ============================================================================= // PHASE 2: Create only mutual pairs @@ -488,7 +504,9 @@ export async function generateMatchesForPrompt( processedPairs.add(pairId); } else { // Non-mutual pair - skip it - console.log(` ✗ Non-mutual: ${userA} → ${userB} (but ${userB} ↛ ${userA})`); + console.log( + ` ✗ Non-mutual: ${userA} → ${userB} (but ${userB} ↛ ${userA})` + ); nonMutualPairsSkipped++; } } @@ -539,7 +557,9 @@ export async function generateMatchesForPrompt( if (matches.length > 0) { try { await createWeeklyMatch(netid, promptId, matches); - console.log(` ✓ Wrote ${matches.length} matches for ${netid}: [${matches.join(', ')}]`); + console.log( + ` ✓ Wrote ${matches.length} matches for ${netid}: [${matches.join(', ')}]` + ); matchedCount++; } catch (error) { console.error(` ✗ Failed to write matches for ${netid}:`, error); @@ -573,7 +593,9 @@ export async function generateMatchesForPrompt( console.log(`${'='.repeat(60)}`); console.log('MATCH GENERATION COMPLETE'); console.log(`${'='.repeat(60)}\n`); - console.log(`✅ ${matchedCount} users successfully matched with guaranteed mutuality\n`); + console.log( + `✅ ${matchedCount} users successfully matched with guaranteed mutuality\n` + ); return matchedCount; } diff --git a/backend/src/services/nudgesService.ts b/backend/src/services/nudgesService.ts index 342230ea..c2066f58 100644 --- a/backend/src/services/nudgesService.ts +++ b/backend/src/services/nudgesService.ts @@ -146,9 +146,7 @@ export async function createNudge( matchFirebaseUid: toFirebaseUid || undefined, } ); - console.log( - `✅ Mutual nudge push notification sent to ${fromNetid}` - ); + console.log(`✅ Mutual nudge push notification sent to ${fromNetid}`); } // Check notification preferences and send to toNetid @@ -172,12 +170,13 @@ export async function createNudge( matchFirebaseUid: fromFirebaseUid || undefined, } ); - console.log( - `✅ Mutual nudge push notification sent to ${toNetid}` - ); + console.log(`✅ Mutual nudge push notification sent to ${toNetid}`); } } catch (notifError) { - console.error('Error sending mutual nudge push notifications:', notifError); + console.error( + 'Error sending mutual nudge push notifications:', + notifError + ); // Don't throw - notification failure shouldn't affect nudge creation } }); diff --git a/backend/src/services/pushNotificationService.ts b/backend/src/services/pushNotificationService.ts index 251ec429..690589ab 100644 --- a/backend/src/services/pushNotificationService.ts +++ b/backend/src/services/pushNotificationService.ts @@ -46,9 +46,7 @@ export async function sendPushNotification( // Check if the push token is valid if (!Expo.isExpoPushToken(pushToken)) { - console.error( - `Invalid push token for user ${netid}: ${pushToken}` - ); + console.error(`Invalid push token for user ${netid}: ${pushToken}`); // Remove invalid token await userSnapshot.docs[0].ref.update({ pushToken: null, @@ -332,8 +330,8 @@ export async function sendBroadcastNotification( // Create in-app notifications in Firestore for all users // Use batch writes for efficiency (max 500 per batch) const BATCH_SIZE = 500; - const allUsers: UserDoc[] = usersSnapshot.docs.map((doc) => - doc.data() as UserDoc + const allUsers: UserDoc[] = usersSnapshot.docs.map( + (doc) => doc.data() as UserDoc ); console.log( @@ -437,7 +435,9 @@ export async function sendBroadcastNotification( const userInfo = userTokenMap.get(globalIndex); if (!userInfo) { - console.error(`No user info found for message index ${globalIndex}`); + console.error( + `No user info found for message index ${globalIndex}` + ); return; } diff --git a/frontend/constants/cornell.ts b/constants/cornell.ts similarity index 98% rename from frontend/constants/cornell.ts rename to constants/cornell.ts index 50df1f10..71c82fb7 100644 --- a/frontend/constants/cornell.ts +++ b/constants/cornell.ts @@ -1,4 +1,4 @@ -import { Gender, School } from '../types'; +import { Gender, School } from 'backend/types'; export const CORNELL_SCHOOLS: School[] = [ 'College of Agriculture and Life Sciences', @@ -147,7 +147,9 @@ export const CORNELL_MAJORS: Record = { }; // Flattened list of all majors for easy selection -export const ALL_MAJORS = Object.values(CORNELL_MAJORS).flat().sort((a, b) => a.localeCompare(b)); +export const ALL_MAJORS = Object.values(CORNELL_MAJORS) + .flat() + .sort((a, b) => a.localeCompare(b)); export const CORNELL_MINORS: string[] = [ 'Actuarial Science', diff --git a/frontend/android/app/google-services.json b/frontend/android/app/google-services.json index 822f8245..59484103 100644 --- a/frontend/android/app/google-services.json +++ b/frontend/android/app/google-services.json @@ -43,4 +43,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/frontend/app/(auth)/(tabs)/chat.tsx b/frontend/app/(auth)/(tabs)/chat.tsx index 9a23e131..2ad12d00 100644 --- a/frontend/app/(auth)/(tabs)/chat.tsx +++ b/frontend/app/(auth)/(tabs)/chat.tsx @@ -16,65 +16,15 @@ import Header from '../../components/ui/Header'; import { useThemeAware } from '../../contexts/ThemeContext'; import { useConversations } from '../../hooks/useConversations'; -// Mock chat data -const mockChats = [ - { - id: '1', - userId: 'mock-user-1', - netid: 'mock-netid-1', - name: 'Emma', - lastMessage: 'Hey! Want to grab coffee at CTB this weekend?', - timestamp: '2m ago', - unread: true, - image: - 'https://media.licdn.com/dms/image/v2/D5603AQFxIrsKx3XV3g/profile-displayphoto-shrink_200_200/B56ZdXeERIHUAg-/0/1749519189434?e=2147483647&v=beta&t=MscfLHknj7AGAwDGZoRcVzT03zerW4P1jUR2mZ3QMKU', - online: true, - }, - { - id: '2', - userId: 'mock-user-2', - netid: 'mock-netid-2', - name: 'Sarah', - lastMessage: 'Thanks for the study session! Good luck on the exam ', - timestamp: '1h ago', - unread: false, - image: - 'https://media.licdn.com/dms/image/v2/D5603AQFxIrsKx3XV3g/profile-displayphoto-shrink_200_200/B56ZdXeERIHUAg-/0/1749519189434?e=2147483647&v=beta&t=MscfLHknj7AGAwDGZoRcVzT03zerW4P1jUR2mZ3QMKU', - online: false, - }, - { - id: '3', - userId: 'mock-user-3', - netid: 'mock-netid-3', - name: 'Jessica', - lastMessage: 'The farmers market was so fun! We should go again', - timestamp: '3h ago', - unread: false, - image: - 'https://media.licdn.com/dms/image/v2/D5603AQFxIrsKx3XV3g/profile-displayphoto-shrink_200_200/B56ZdXeERIHUAg-/0/1749519189434?e=2147483647&v=beta&t=MscfLHknj7AGAwDGZoRcVzT03zerW4P1jUR2mZ3QMKU', - online: true, - }, - { - id: '4', - userId: 'mock-user-4', - netid: 'mock-netid-4', - name: 'Alex', - lastMessage: 'Are you free for lunch tomorrow?', - timestamp: '1d ago', - unread: true, - image: - 'https://media.licdn.com/dms/image/v2/D5603AQFxIrsKx3XV3g/profile-displayphoto-shrink_200_200/B56ZdXeERIHUAg-/0/1749519189434?e=2147483647&v=beta&t=MscfLHknj7AGAwDGZoRcVzT03zerW4P1jUR2mZ3QMKU', - online: false, - }, -]; - export default function ChatScreen() { useThemeAware(); // Force re-render when theme changes - const { conversations, loading, error } = useConversations(); + const { conversations, loading } = useConversations(); const currentUser = getCurrentUser(); const [blockedUsers, setBlockedUsers] = useState>(new Set()); const [animationTrigger, setAnimationTrigger] = useState(0); - const [freshProfiles, setFreshProfiles] = useState>({}); + const [freshProfiles, setFreshProfiles] = useState< + Record + >({}); // Fetch blocked users list when screen is focused useFocusEffect( @@ -127,55 +77,63 @@ export default function ChatScreen() { const chatData = useMemo(() => { if (!currentUser) { } - if (!currentUser) return mockChats; + if (!currentUser) return []; console.log(currentUser.uid); - return conversations - .map((conv) => { - // Get the other participant's info - const otherUserId = conv.participantIds.find( - (id) => id !== currentUser.uid - ); - const otherUser = otherUserId ? conv.participants[otherUserId] : null; - - // Get fresh profile data if available, otherwise use cached data - const freshProfile = otherUserId ? freshProfiles[otherUserId] : null; - - // Use fresh profile picture (pictures[0]) if available, otherwise fall back to cached image - // If no image available, use placeholder - const profileImage = freshProfile?.pictures?.[0] || otherUser?.image || 'https://via.placeholder.com/150'; - - // Format timestamp - let timestamp = 'Just now'; - if (conv.lastMessage?.timestamp) { - const messageDate = - conv.lastMessage.timestamp.toDate?.() || - new Date(conv.lastMessage.timestamp); - const now = new Date(); - const diffMs = now.getTime() - messageDate.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) timestamp = 'Just now'; - else if (diffMins < 60) timestamp = `${diffMins}m ago`; - else if (diffHours < 24) timestamp = `${diffHours}h ago`; - else timestamp = `${diffDays}d ago`; - } + return conversations.map((conv) => { + // Get the other participant's info + const otherUserId = conv.participantIds.find( + (id) => id !== currentUser.uid + ); + const otherUser = otherUserId ? conv.participants[otherUserId] : null; + + // Get fresh profile data if available, otherwise use cached data + const freshProfile = otherUserId ? freshProfiles[otherUserId] : null; + + // Use fresh profile picture (pictures[0]) if available, otherwise fall back to cached image + // If no image available, use placeholder + const profileImage = + freshProfile?.pictures?.[0] || + otherUser?.image || + 'https://via.placeholder.com/150'; + + // Format timestamp + let timestamp = 'Just now'; + if (conv.lastMessage?.timestamp) { + const messageDate = + conv.lastMessage.timestamp.toDate?.() || + new Date(conv.lastMessage.timestamp); + const now = new Date(); + const diffMs = now.getTime() - messageDate.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) timestamp = 'Just now'; + else if (diffMins < 60) timestamp = `${diffMins}m ago`; + else if (diffHours < 24) timestamp = `${diffHours}h ago`; + else timestamp = `${diffDays}d ago`; + } - return { - id: conv.id, - userId: otherUserId || '', - netid: freshProfile?.netid || otherUser?.netid || '', - name: otherUser?.deleted ? 'Deleted User' : (freshProfile?.firstName || otherUser?.name || 'Unknown'), - lastMessage: conv.lastMessage?.text || 'Start a conversation', - timestamp, - image: profileImage, - }; - }); + return { + id: conv.id, + userId: otherUserId || '', + netid: freshProfile?.netid || otherUser?.netid || '', + name: otherUser?.deleted + ? 'Deleted User' + : freshProfile?.firstName || otherUser?.name || 'Unknown', + lastMessage: conv.lastMessage?.text || 'Start a conversation', + timestamp, + image: profileImage, + }; + }); }, [conversations, currentUser, freshProfiles]); - const displayData = chatData; + const displayData = useMemo(() => { + return chatData.filter((chat) => { + return chat.netid && !blockedUsers.has(chat.netid); + }); + }, [chatData, blockedUsers]); if (loading) { return ( diff --git a/frontend/app/(auth)/(tabs)/profile.tsx b/frontend/app/(auth)/(tabs)/profile.tsx index 8c7bc666..3b76d6e3 100644 --- a/frontend/app/(auth)/(tabs)/profile.tsx +++ b/frontend/app/(auth)/(tabs)/profile.tsx @@ -35,7 +35,11 @@ import { StyleSheet, View, } from 'react-native'; -import { clearUserStorage, getCurrentUser, signOutUser } from '../../api/authService'; +import { + clearUserStorage, + getCurrentUser, + signOutUser, +} from '../../api/authService'; import { AppColors } from '../../components/AppColors'; import { useProfile } from '../../contexts/ProfileContext'; import { useThemeAware } from '../../contexts/ThemeContext'; @@ -78,7 +82,7 @@ export default function ProfileScreen() { Alert.alert( 'Error', 'Failed to sign out: ' + - (error instanceof Error ? error.message : 'Unknown error') + (error instanceof Error ? error.message : 'Unknown error') ); } }; @@ -131,7 +135,9 @@ export default function ProfileScreen() { - {displayName} + + {displayName} + Member since {getMemberSinceText()} diff --git a/frontend/app/(auth)/account-settings.tsx b/frontend/app/(auth)/account-settings.tsx index 828a2dbb..001038a6 100644 --- a/frontend/app/(auth)/account-settings.tsx +++ b/frontend/app/(auth)/account-settings.tsx @@ -66,7 +66,7 @@ export default function AccountSettingsPage() { Alert.alert( 'Error', 'Failed to sign out: ' + - (error instanceof Error ? error.message : 'Unknown error') + (error instanceof Error ? error.message : 'Unknown error') ); } }; @@ -115,7 +115,7 @@ export default function AccountSettingsPage() { Alert.alert( 'Error', 'Failed to delete account: ' + - (error instanceof Error ? error.message : 'Unknown error') + (error instanceof Error ? error.message : 'Unknown error') ); } }; @@ -153,7 +153,7 @@ export default function AccountSettingsPage() { iconLeft={Pencil} variant="secondary" title="Cannot change email address" - onPress={() => { }} + onPress={() => {}} /> diff --git a/frontend/app/(auth)/create-profile.tsx b/frontend/app/(auth)/create-profile.tsx index e34d78fc..ecfeec8a 100644 --- a/frontend/app/(auth)/create-profile.tsx +++ b/frontend/app/(auth)/create-profile.tsx @@ -30,7 +30,7 @@ import Animated, { withSpring, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { CORNELL_MAJORS, CORNELL_SCHOOLS } from '../../constants/cornell'; +import { CORNELL_MAJORS, CORNELL_SCHOOLS } from '../../../constants/cornell'; import { getCurrentUser } from '../api/authService'; import { uploadImages } from '../api/imageApi'; import { updatePreferences } from '../api/preferencesApi'; diff --git a/frontend/app/(auth)/edit-education.tsx b/frontend/app/(auth)/edit-education.tsx index 32b8fb5c..3469cdee 100644 --- a/frontend/app/(auth)/edit-education.tsx +++ b/frontend/app/(auth)/edit-education.tsx @@ -9,9 +9,9 @@ import { CORNELL_MAJORS, CORNELL_MINORS, CORNELL_SCHOOLS, - Year, YEARS, -} from '../../constants/cornell'; + Year, +} from '../../../constants/cornell'; import { getCurrentUser } from '../api/authService'; import { updateProfile } from '../api/profileApi'; import { AppColors } from '../components/AppColors'; diff --git a/frontend/app/(auth)/edit-profile.tsx b/frontend/app/(auth)/edit-profile.tsx index 5daa8cfa..81f1ee98 100644 --- a/frontend/app/(auth)/edit-profile.tsx +++ b/frontend/app/(auth)/edit-profile.tsx @@ -10,14 +10,7 @@ import { Plus, } from 'lucide-react-native'; import React, { useEffect, useRef, useState } from 'react'; -import { - Alert, - Linking, - ScrollView, - StatusBar, - StyleSheet, - View, -} from 'react-native'; +import { Alert, ScrollView, StatusBar, StyleSheet, View } from 'react-native'; import { GestureDetector } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -99,7 +92,6 @@ export default function EditProfileScreen() { const haptic = useHapticFeedback(); const { profile: profileData, - loading, refreshProfile, updateProfileData, } = useProfile(); @@ -120,7 +112,6 @@ export default function EditProfileScreen() { // Scroll position preservation const scrollViewRef = useRef(null); - const scrollPositionRef = useRef(0); // Initialize data from profile context useEffect(() => { @@ -153,20 +144,6 @@ export default function EditProfileScreen() { } }, [profileData]); - const openURL = async (url: string) => { - try { - const supported = await Linking.canOpenURL(url); - if (supported) { - await Linking.openURL(url); - } else { - Alert.alert('Error', `Cannot open URL: ${url}`); - } - } catch (error) { - Alert.alert('Error', 'Failed to open link'); - console.error('Error opening URL:', error); - } - }; - const hasUnsavedChanges = () => { const promptsChanged = JSON.stringify(prompts) !== JSON.stringify(originalPrompts); @@ -318,25 +295,6 @@ export default function EditProfileScreen() { } }; - const addPrompt = () => { - if (prompts.length < 3) { - const newPrompt: PromptData = { - id: Date.now().toString(), - question: '', - answer: '', - }; - setPrompts([...prompts, newPrompt]); - } - }; - - const updatePrompt = (id: string, updatedPrompt: PromptData) => { - setPrompts(prompts.map((p) => (p.id === id ? updatedPrompt : p))); - }; - - const removePrompt = (id: string) => { - setPrompts(prompts.filter((p) => p.id !== id)); - }; - const reorderPrompts = (fromIndex: number, toIndex: number) => { if (fromIndex === toIndex) return; @@ -349,17 +307,10 @@ export default function EditProfileScreen() { // Get display data - use profile data if available, otherwise fallback const displayName = profileData?.firstName || 'User'; const displayAge = profileData ? `${getProfileAge(profileData)}` : 'Not set'; - const displayBio = profileData?.bio || 'No bio yet'; - const displaySchool = profileData?.school || 'School not set'; - const displayMajor = - profileData?.major && profileData.major.length > 0 - ? profileData.major.join(', ') - : 'Major not set'; const displayMinor = profileData?.minor && profileData.minor.length > 0 ? profileData.minor.join(', ') : null; - const displayYear = profileData?.year || 'Year not set'; // Social fields are only available on OwnProfileResponse const displayInstagram = profileData && 'instagram' in profileData @@ -381,10 +332,6 @@ export default function EditProfileScreen() { : null; const displayClubs = profileData?.clubs || []; const displayInterests = profileData?.interests || []; - const displayEthnicity = - profileData?.ethnicity && profileData.ethnicity.length > 0 - ? profileData.ethnicity.join(', ') - : null; // Get socials order from profile, or use default order const socialsOrder = @@ -440,10 +387,6 @@ export default function EditProfileScreen() { contentContainerStyle={{ rowGap: 24, }} - onScroll={(event) => { - scrollPositionRef.current = event.nativeEvent.contentOffset.y; - }} - scrollEventThrottle={16} >