diff --git a/.vscode/launch.json b/.vscode/launch.json index b2f71611..0c661bc1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,12 +5,13 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug Jest Tests", + "name": "Debug Current Test", "type": "node", "request": "launch", "runtimeArgs": [ "--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", + "${fileBasename}", "--runInBand" ], "console": "integratedTerminal", diff --git a/backend/api/tests/unit/create-profile.unit.test.ts b/backend/api/tests/unit/create-profile.unit.test.ts index 50842fec..77dd3d4f 100644 --- a/backend/api/tests/unit/create-profile.unit.test.ts +++ b/backend/api/tests/unit/create-profile.unit.test.ts @@ -6,6 +6,10 @@ jest.mock('shared/supabase/utils'); jest.mock('common/util/try-catch'); jest.mock('shared/analytics'); jest.mock('common/discord/core'); +jest.mock('common/util/time', () => { + const actual = jest.requireActual('common/util/time'); + return{ ...actual, sleep: () => Promise.resolve()} +}); import { createProfile } from "api/create-profile"; import * as supabaseInit from "shared/supabase/init"; @@ -52,7 +56,7 @@ describe('createProfile', () => { }; const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - const mockExistingUser = {id: "mockExistingUserId"}; + const mockNProfiles = 10 const mockData = { age: 30, city: "mockCity" @@ -68,12 +72,268 @@ describe('createProfile', () => { (supabaseUsers.updateUser as jest.Mock).mockReturnValue(null); (supabaseUtils.insert as jest.Mock).mockReturnValue(null); (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}); + + const results: any = await createProfile(mockBody, mockAuth, mockReq); + + expect(results.result).toEqual(mockData); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) + expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); - (sendDiscordMessage as jest.Mock).mockResolvedValueOnce(null); - (mockPg.one as jest.Mock).mockReturnValue(10); + (sendDiscordMessage as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (mockPg.one as jest.Mock).mockReturnValue(mockNProfiles); + + await results.continue(); + + expect(sharedAnalytics.track).toBeCalledTimes(1); + expect(sharedAnalytics.track).toBeCalledWith( + mockAuth.uid, + 'create profile', + {username: mockUser.username} + ); + expect(sendDiscordMessage).toBeCalledTimes(1); + expect(sendDiscordMessage).toBeCalledWith( + expect.stringContaining(mockUser.name && mockUser.username), + 'members' + ); + }); + + it('successfully create milestone profile', async () => { + const mockBody = { + city: "mockCity", + gender: "mockGender", + looking_for_matches: true, + photo_urls: ["mockPhotoUrl1"], + pinned_url: "mockPinnedUrl", + pref_gender: ["mockPrefGender"], + pref_relation_styles: ["mockPrefRelationStyles"], + visibility: 'public' as "public" | "member", + wants_kids_strength: 2, + }; + const mockAuth = {uid: '321'} as AuthedUser; + const mockReq = {} as any; + const mockNProfiles = 15 + const mockData = { + age: 30, + city: "mockCity" + }; + const mockUser = { + createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago + name: "mockName", + username: "mockUserName" + }; + + (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (supabaseUsers.updateUser as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}); const results: any = await createProfile(mockBody, mockAuth, mockReq); - expect(results.result).toEqual(mockData) + + expect(results.result).toEqual(mockData); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) + expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); + (sendDiscordMessage as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (mockPg.one as jest.Mock).mockReturnValue(mockNProfiles); + + await results.continue(); + + expect(sharedAnalytics.track).toBeCalledTimes(1); + expect(sharedAnalytics.track).toBeCalledWith( + mockAuth.uid, + 'create profile', + {username: mockUser.username} + ); + expect(sendDiscordMessage).toBeCalledTimes(2); + expect(sendDiscordMessage).toHaveBeenNthCalledWith( + 1, + expect.stringContaining(mockUser.name && mockUser.username), + 'members' + ); + expect(sendDiscordMessage).toHaveBeenNthCalledWith( + 2, + expect.stringContaining(String(mockNProfiles)), + 'general' + ); + + }); + + it('throws an error if it failed to track create profile', async () => { + const mockBody = { + city: "mockCity", + gender: "mockGender", + looking_for_matches: true, + photo_urls: ["mockPhotoUrl1"], + pinned_url: "mockPinnedUrl", + pref_gender: ["mockPrefGender"], + pref_relation_styles: ["mockPrefRelationStyles"], + visibility: 'public' as "public" | "member", + wants_kids_strength: 2, + }; + const mockAuth = {uid: '321'} as AuthedUser; + const mockReq = {} as any; + const mockData = { + age: 30, + city: "mockCity" + }; + const mockUser = { + createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago + name: "mockName", + username: "mockUserName" + }; + + (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (supabaseUsers.updateUser as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}); + + const results: any = await createProfile(mockBody, mockAuth, mockReq); + + expect(results.result).toEqual(mockData); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) + expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + + const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {}); + + (sharedAnalytics.track as jest.Mock).mockRejectedValue(null); + + await results.continue(); + expect(errorSpy).toBeCalledWith('Failed to track create profile', null) + }); + + it('throws an error if it failed to send discord new profile', async () => { + const mockBody = { + city: "mockCity", + gender: "mockGender", + looking_for_matches: true, + photo_urls: ["mockPhotoUrl1"], + pinned_url: "mockPinnedUrl", + pref_gender: ["mockPrefGender"], + pref_relation_styles: ["mockPrefRelationStyles"], + visibility: 'public' as "public" | "member", + wants_kids_strength: 2, + }; + const mockAuth = {uid: '321'} as AuthedUser; + const mockReq = {} as any; + const mockData = { + age: 30, + city: "mockCity" + }; + const mockUser = { + createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago + name: "mockName", + username: "mockUserName" + }; + + (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (supabaseUsers.updateUser as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}); + + const results: any = await createProfile(mockBody, mockAuth, mockReq); + + expect(results.result).toEqual(mockData); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) + expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + + const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {}); + + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); + (sendDiscordMessage as jest.Mock).mockRejectedValue(null); + + await results.continue(); + expect(sharedAnalytics.track).toBeCalledTimes(1); + expect(sharedAnalytics.track).toBeCalledWith( + mockAuth.uid, + 'create profile', + {username: mockUser.username} + ); + expect(errorSpy).toBeCalledWith('Failed to send discord new profile', null); + }); + + it('throws an error if it failed to send discord user milestone', async () => { + const mockBody = { + city: "mockCity", + gender: "mockGender", + looking_for_matches: true, + photo_urls: ["mockPhotoUrl1"], + pinned_url: "mockPinnedUrl", + pref_gender: ["mockPrefGender"], + pref_relation_styles: ["mockPrefRelationStyles"], + visibility: 'public' as "public" | "member", + wants_kids_strength: 2, + }; + const mockAuth = {uid: '321'} as AuthedUser; + const mockReq = {} as any; + const mockNProfiles = 15 + const mockData = { + age: 30, + city: "mockCity" + }; + const mockUser = { + createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago + name: "mockName", + username: "mockUserName" + }; + + (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (supabaseUsers.updateUser as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}); + + const results: any = await createProfile(mockBody, mockAuth, mockReq); + + expect(results.result).toEqual(mockData); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) + expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + + const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {}); + + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); + (sendDiscordMessage as jest.Mock) + .mockResolvedValueOnce(null) + .mockRejectedValueOnce(null); + (mockPg.one as jest.Mock).mockReturnValue(mockNProfiles); + + await results.continue(); + expect(sharedAnalytics.track).toBeCalledTimes(1); + expect(sharedAnalytics.track).toBeCalledWith( + mockAuth.uid, + 'create profile', + {username: mockUser.username} + ); + expect(sendDiscordMessage).toBeCalledTimes(2); + expect(sendDiscordMessage).toHaveBeenNthCalledWith( + 1, + expect.stringContaining(mockUser.name && mockUser.username), + 'members' + ); + expect(sendDiscordMessage).toHaveBeenNthCalledWith( + 2, + expect.stringContaining(String(mockNProfiles)), + 'general' + ); + expect(errorSpy).toBeCalledWith('Failed to send discord user milestone', null); }); it('throws an error if the profile already exists', async () => { diff --git a/backend/api/tests/unit/create-user.unit.test.ts b/backend/api/tests/unit/create-user.unit.test.ts index afc32b73..469f99ad 100644 --- a/backend/api/tests/unit/create-user.unit.test.ts +++ b/backend/api/tests/unit/create-user.unit.test.ts @@ -1,7 +1,835 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/utils'); +jest.mock('common/supabase/users'); +jest.mock('email/functions/helpers'); +jest.mock('api/set-last-online-time'); +jest.mock('firebase-admin', () => ({ + auth: jest.fn() +})); +jest.mock('shared/utils'); +jest.mock('shared/analytics'); +jest.mock('shared/firebase-utils'); +jest.mock('shared/helpers/generate-and-update-avatar-urls'); +jest.mock('common/util/object'); +jest.mock('common/user-notification-preferences'); +jest.mock('common/util/clean-username'); +jest.mock('shared/monitoring/log'); +jest.mock('common/hosting/constants'); + +import { createUser } from "api/create-user"; +import * as supabaseInit from "shared/supabase/init"; +import * as supabaseUtils from "shared/supabase/utils"; +import * as supabaseUsers from "common/supabase/users"; +import * as emailHelpers from "email/functions/helpers"; +import * as apiSetLastTimeOnline from "api/set-last-online-time"; +import * as firebaseAdmin from "firebase-admin"; +import * as sharedUtils from "shared/utils"; +import * as sharedAnalytics from "shared/analytics"; +import * as firebaseUtils from "shared/firebase-utils"; +import * as avatarHelpers from "shared/helpers/generate-and-update-avatar-urls"; +import * as objectUtils from "common/util/object"; +import * as userNotificationPref from "common/user-notification-preferences"; +import * as usernameUtils from "common/util/clean-username"; +import * as hostingConstants from "common/hosting/constants"; +import { AuthedUser } from "api/helpers/endpoint"; + + describe('createUser', () => { - describe('should', () => { - it('', async () => { + const originalIsLocal = (hostingConstants as any).IS_LOCAL; + let mockPg = {} as any; + + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + one: jest.fn(), + tx: jest.fn(async (cb) => { + const mockTx = {} as any; + return cb(mockTx) + }) + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + + afterEach(() => { + jest.restoreAllMocks(); + Object.defineProperty(hostingConstants, 'IS_LOCAL', { + value: originalIsLocal, + writable: true, + }); + }); + + describe('when given valid input', () => { + it('should successfully create a user', async () => { + Object.defineProperty(hostingConstants, 'IS_LOCAL', { + value: false, + writable: true + }); + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + const results: any = await createUser(mockProps, mockAuth, mockReq); + + expect(results.result.user).toEqual(mockNewUserRow); + expect(results.result.privateUser).toEqual(mockPrivateUserRow); + expect(mockGetUser).toBeCalledTimes(2); + expect(mockGetUser).toHaveBeenNthCalledWith(1, mockAuth.uid); + expect(mockReq.get).toBeCalledTimes(1); + expect(mockReq.get).toBeCalledWith(Object.keys(mockReferer.headers)[0]); + expect(sharedAnalytics.getIp).toBeCalledTimes(1); + expect(sharedAnalytics.getIp).toBeCalledWith(mockReq); + expect(mockGetUser).toHaveBeenNthCalledWith(2, mockAuth.uid); + expect(usernameUtils.cleanDisplayName).toBeCalledTimes(1); + expect(usernameUtils.cleanDisplayName).toHaveBeenCalledWith(mockFbUser.displayName); + expect(usernameUtils.cleanUsername).toBeCalledTimes(1); + expect(usernameUtils.cleanUsername).toBeCalledWith(mockFbUser.displayName); + expect(mockPg.one).toBeCalledTimes(1); + expect(mockPg.tx).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toHaveBeenCalledWith( + mockAuth.uid, + expect.any(Object) + ); + expect(userNotificationPref.getDefaultNotificationPreferences).toBeCalledTimes(1); + expect(supabaseUtils.insert).toBeCalledTimes(2); + expect(supabaseUtils.insert).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + 'users', + expect.objectContaining( + { + id: mockAuth.uid, + name: mockFbUser.displayName, + username: mockFbUser.displayName, + } + ) + ); + expect(supabaseUtils.insert).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + 'private_users', + expect.objectContaining( + { + id: mockAuth.uid, + } + ) + ); + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); + (emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null); + (apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockResolvedValue(null); + + await results.continue(); + + expect(sharedAnalytics.track).toBeCalledTimes(1); + expect(sharedAnalytics.track).toBeCalledWith( + mockAuth.uid, + 'create profile', + {username: mockNewUserRow.username} + ); + expect(emailHelpers.sendWelcomeEmail).toBeCalledTimes(1); + expect(emailHelpers.sendWelcomeEmail).toBeCalledWith(mockNewUserRow, mockPrivateUserRow); + expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledTimes(1); + expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledWith(mockAuth.uid); + }); + + it('should generate a device token when creating a user', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'password' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + await createUser(mockProps, mockAuth, mockReq); + + expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith( + 2, + expect.any(Object), + 'private_users', + { + id: expect.any(String), + data: expect.objectContaining( + { + initialDeviceToken: mockProps.deviceToken + } + ) + } + ); + + }); + + it('should generate a avatar Url when creating a user', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'password' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockAvatarUrl = "mockGeneratedAvatarUrl" + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (avatarHelpers.generateAvatarUrl as jest.Mock).mockResolvedValue(mockAvatarUrl); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + await createUser(mockProps, mockAuth, mockReq); + + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1); + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith( + { + avatarUrl: mockAvatarUrl, + isBannedFromPosting: false, + link: expect.any(Object) + } + ); + + }); + + it('should not allow a username that already exists when creating a user', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(1); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + await createUser(mockProps, mockAuth, mockReq); + + expect(mockPg.one).toBeCalledTimes(1); + expect(supabaseUtils.insert).toBeCalledTimes(2); + expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith( + 1, + expect.any(Object), + 'users', + expect.objectContaining( + { + id: mockAuth.uid, + name: mockFbUser.displayName, + username: mockFbUser.displayName, + } + ) + ); + }); + + it('should successfully create a user who is banned from posting if there ip/device token is banned', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + await createUser(mockProps, mockAuth, mockReq); + + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1); + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith( + { + avatarUrl: mockFbUser.photoURL, + isBannedFromPosting: true, + link: expect.any(Object) + } + ); + }); + }); + + describe('when an error occurs', () => { + it('should throw an error if the user already exists', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + + expect(createUser(mockProps, mockAuth, mockReq)) + .rejects + .toThrowError('User already exists'); + }); + + it('should throw an error if the username is already taken', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(true); + + expect(createUser(mockProps, mockAuth, mockReq)) + .rejects + .toThrowError('Username already taken'); + }); + + it('should throw an error if failed to track create profile', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + const results: any = await createUser(mockProps, mockAuth, mockReq); + + (sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Tracking failed')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await results.continue(); + + expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error)); + }); + + it('should throw an error if failed to send a welcome email', async () => { + Object.defineProperty(hostingConstants, 'IS_LOCAL', { + value: false, + writable: true + }); + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + const results: any = await createUser(mockProps, mockAuth, mockReq); + + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); + (emailHelpers.sendWelcomeEmail as jest.Mock).mockRejectedValue(new Error('Welcome email failed')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await results.continue(); + + expect(errorSpy).toBeCalledWith('Failed to sendWelcomeEmail', expect.any(Error)); + }); + + it('should throw an error if failed to set last time online', async () => { + const mockProps = { + deviceToken: "mockDeviceToken", + adminToken: "mockAdminToken" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReferer = { + headers: { + 'referer': 'mockReferer' + } + }; + const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any; + const mockFirebaseUser = { + providerData: [ + { + providerId: 'passwords' + } + ], + }; + const mockFbUser = { + email: "mockEmail@mockServer.com", + displayName: "mockDisplayName", + photoURL: "mockPhotoUrl" + }; + const mockIp = "mockIP"; + const mockBucket = {} as any; + const mockNewUserRow = { + created_time: "mockCreatedTime", + data: {"mockNewUserJson": "mockNewUserJsonData"}, + id: "mockNewUserId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername" + }; + const mockPrivateUserRow = { + data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"}, + id: "mockPrivateUserId" + }; + + const mockGetUser = jest.fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser); + + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + getUser: mockGetUser + }); + (usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName); + (firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket); + (usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName); + (mockPg.one as jest.Mock).mockResolvedValue(0); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false); + (userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null); + (supabaseUtils.insert as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow); + (supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow); + + const results: any = await createUser(mockProps, mockAuth, mockReq); + + (sharedAnalytics.track as jest.Mock).mockResolvedValue(null); + (emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null); + (apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockRejectedValue(new Error('Failed to set last online time')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await results.continue(); + expect(errorSpy).toHaveBeenCalledWith('Failed to set last online time', expect.any(Error)); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/create-vote.unit.test.ts b/backend/api/tests/unit/create-vote.unit.test.ts new file mode 100644 index 00000000..03e42e72 --- /dev/null +++ b/backend/api/tests/unit/create-vote.unit.test.ts @@ -0,0 +1,98 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/utils'); +jest.mock('shared/supabase/utils'); +jest.mock('common/util/try-catch'); + +import { createVote } from "api/create-vote"; +import * as supabaseInit from "shared/supabase/init"; +import * as sharedUtils from "shared/utils"; +import * as supabaseUtils from "shared/supabase/utils"; +import { tryCatch } from "common/util/try-catch"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('createVote', () => { + beforeEach(() => { + jest.resetAllMocks(); + const mockPg = {} as any; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg) + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('successfully creates a vote', async () => { + const mockProps = { + title: 'mockTitle', + description: {'mockDescription': 'mockDescriptionValue'}, + isAnonymous: true + }; + const mockCreator = {id: '123'}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = { + creator_id: mockCreator.id, + title: 'mockTitle', + description: {'mockDescription': 'mockDescriptionValue'}, + is_anonymous: true, + status: 'voting_open' + }; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); + (tryCatch as jest.Mock).mockResolvedValue({data: mockData , error: null}); + + const result = await createVote(mockProps, mockAuth, mockReq); + expect(result.data).toEqual(mockData); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(supabaseUtils.insert).toBeCalledTimes(1); + expect(supabaseUtils.insert).toHaveBeenCalledWith( + expect.any(Object), + 'votes', + { + creator_id: mockCreator.id, + title: mockProps.title, + description: mockProps.description, + is_anonymous: mockProps.isAnonymous, + status: 'voting_open' + } + ); + }); + }); + describe('when an error occurs', () => { + it('should throw an error if the account was not found', async () => { + const mockProps = { + title: 'mockTitle', + description: {'mockDescription': 'mockDescriptionValue'}, + isAnonymous: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + + expect(createVote(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Your account was not found'); + }); + + it('should throw an error if unable to create a question', async () => { + const mockProps = { + title: 'mockTitle', + description: {'mockDescription': 'mockDescriptionValue'}, + isAnonymous: true + }; + const mockCreator = {id: '123'}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); + (tryCatch as jest.Mock).mockResolvedValue({data: null , error: Error}); + + expect(createVote(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Error creating question'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts b/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts new file mode 100644 index 00000000..3f9beed9 --- /dev/null +++ b/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts @@ -0,0 +1,42 @@ +jest.mock('shared/supabase/init'); + +import { deleteBookmarkedSearch } from "api/delete-bookmarked-search"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('deleteBookmarkedSearch', () => { + let mockPg = {} as any; + + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + none: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('successfully deletes a bookmarked search', async () => { + const mockProps = { + id: 123 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + const result = await deleteBookmarkedSearch(mockProps, mockAuth, mockReq); + expect(result).toStrictEqual({}); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('DELETE FROM bookmarked_searches'), + [ + mockProps.id, + mockAuth.uid + ] + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts b/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts new file mode 100644 index 00000000..15f71151 --- /dev/null +++ b/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts @@ -0,0 +1,71 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/compatibility/compute-scores'); + +import { deleteCompatibilityAnswer } from "api/delete-compatibility-answer"; +import * as supabaseInit from "shared/supabase/init"; +import { recomputeCompatibilityScoresForUser } from "shared/compatibility/compute-scores"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('deleteCompatibilityAnswers', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should successfully delete compatibility answers', async () => { + const mockProps = { + id: 123 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(true); + (mockPg.none as jest.Mock).mockResolvedValue(null); + + const results: any = await deleteCompatibilityAnswer(mockProps, mockAuth, mockReq); + + expect(results.status).toBe('success'); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining(`SELECT *`), + [mockProps.id, mockAuth.uid] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('DELETE'), + [mockProps.id, mockAuth.uid] + ); + + await results.continue(); + + (recomputeCompatibilityScoresForUser as jest.Mock).mockResolvedValue(null); + expect(recomputeCompatibilityScoresForUser).toBeCalledTimes(1); + expect(recomputeCompatibilityScoresForUser).toBeCalledWith(mockAuth.uid, expect.any(Object)); + }); + }); + describe('when an error occurs', () => { + it('should throw an error if the user is not the answers author', async () => { + const mockProps = { + id: 123 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + expect(deleteCompatibilityAnswer(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Item not found'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/delete-me.unit.test.ts b/backend/api/tests/unit/delete-me.unit.test.ts new file mode 100644 index 00000000..12892165 --- /dev/null +++ b/backend/api/tests/unit/delete-me.unit.test.ts @@ -0,0 +1,126 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/utils'); +jest.mock('firebase-admin', () => ({ + auth: jest.fn() +})); +jest.mock('shared/firebase-utils'); + +import { deleteMe } from "api/delete-me"; +import * as supabaseInit from "shared/supabase/init"; +import * as sharedUtils from "shared/utils"; +import * as firebaseAdmin from "firebase-admin"; +import * as firebaseUtils from "shared/firebase-utils"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('deleteMe', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + none: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg) + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('when given valid input', () => { + it('should delete the user account from supabase and firebase', async () => { + const mockUser = { + id: "mockId", + username: "mockUsername" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockRef = {} as any; + + const mockDeleteUser = jest.fn().mockResolvedValue(null); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (firebaseUtils.deleteUserFiles as jest.Mock).mockResolvedValue(null); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + deleteUser: mockDeleteUser + }); + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + + await deleteMe(mockRef, mockAuth, mockRef); + + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('DELETE FROM users WHERE id = $1'), + [mockUser.id] + ); + expect(firebaseUtils.deleteUserFiles).toBeCalledTimes(1); + expect(firebaseUtils.deleteUserFiles).toBeCalledWith(mockUser.username); + expect(mockDeleteUser).toBeCalledTimes(1); + expect(mockDeleteUser).toBeCalledWith(mockUser.id); + + expect(debugSpy).toBeCalledWith( + expect.stringContaining(mockUser.id) + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if the user account was not found', async () => { + const mockUser = { + id: "mockId", + username: "mockUsername" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockRef = {} as any; + + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + + expect(deleteMe(mockRef, mockAuth, mockRef)) + .rejects + .toThrow('Your account was not found'); + + }); + + it('should throw an error if there is no userId', async () => { + const mockUser = { + username: "mockUsername" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockRef = {} as any; + + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + + + expect(deleteMe(mockRef, mockAuth, mockRef)) + .rejects + .toThrow('Invalid user ID'); + + }); + + it('should throw if unable to remove user from firebase auth', async () => { + const mockUser = { + id: "mockId", + username: "mockUsername" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockRef = {} as any; + + const mockDeleteUser = jest.fn().mockRejectedValue(new Error('Error during deletion')); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (firebaseUtils.deleteUserFiles as jest.Mock).mockResolvedValue(null); + (firebaseAdmin.auth as jest.Mock).mockReturnValue({ + deleteUser: mockDeleteUser + }); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await deleteMe(mockRef, mockAuth, mockRef); + + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Error deleting user from Firebase Auth:'), + expect.any(Error) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/delete-message.unit.test.ts b/backend/api/tests/unit/delete-message.unit.test.ts new file mode 100644 index 00000000..e4ed96b0 --- /dev/null +++ b/backend/api/tests/unit/delete-message.unit.test.ts @@ -0,0 +1,99 @@ +jest.mock('shared/supabase/init'); +jest.mock('api/helpers/private-messages'); + +import { deleteMessage } from "api/delete-message"; +import * as supabaseInit from "shared/supabase/init"; +import * as messageHelpers from "api/helpers/private-messages"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('deleteMessage', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn() + }; + + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('when given valid input', () => { + it('should delete a message', async () => { + const mockMessageId = { + messageId: 123 + }; + const mockMessage = { + channel_id: "mockChannelId" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null); + + const results = await deleteMessage(mockMessageId, mockAuth, mockReq); + expect(results.success).toBeTruthy(); + + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('SELECT *'), + [mockMessageId.messageId, mockAuth.uid] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('DELETE'), + [mockMessageId.messageId, mockAuth.uid] + ); + expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1); + expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith( + expect.any(Object), + mockMessage.channel_id, + mockAuth.uid + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if the message was not found', async () => { + const mockMessageId = { + messageId: 123 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + + expect(deleteMessage(mockMessageId, mockAuth, mockReq)) + .rejects + .toThrow('Message not found'); + }); + + it('should throw if the message was not broadcasted', async () => { + const mockMessageId = { + messageId: 123 + }; + const mockMessage = { + channel_id: "mockChannelId" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.broadcastPrivateMessages as jest.Mock).mockRejectedValue(new Error('Broadcast Error')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await deleteMessage(mockMessageId, mockAuth, mockReq); + + expect(errorSpy).toBeCalledTimes(1); + expect(errorSpy).toBeCalledWith( + expect.stringContaining('broadcastPrivateMessages failed'), + expect.any(Error) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/edit-message.unit.test.ts b/backend/api/tests/unit/edit-message.unit.test.ts new file mode 100644 index 00000000..402777bd --- /dev/null +++ b/backend/api/tests/unit/edit-message.unit.test.ts @@ -0,0 +1,125 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/encryption'); +jest.mock('api/helpers/private-messages'); + +import { editMessage } from "api/edit-message"; +import * as supabaseInit from "shared/supabase/init"; +import * as encryptionModules from "shared/encryption"; +import * as messageHelpers from "api/helpers/private-messages"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('editMessage', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should edit the messages associated with the messageId', async () => { + const mockProps = { + messageId: 123, + content: {'mockContent' : 'mockContentValue'} + }; + const mockPlainTextContent = JSON.stringify(mockProps.content) + const mockMessage = { + channel_id: "mockChannelId" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockCipher = "mockCipherText"; + const mockIV = "mockIV"; + const mockTag = "mockTag"; + const mockEncryption = { + ciphertext: mockCipher, + iv: mockIV, + tag: mockTag + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); + (encryptionModules.encryptMessage as jest.Mock).mockReturnValue(mockEncryption); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null); + + const result = await editMessage(mockProps, mockAuth, mockReq); + + expect(result.success).toBeTruthy(); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('SELECT *'), + [mockProps.messageId, mockAuth.uid] + ); + expect(encryptionModules.encryptMessage).toBeCalledTimes(1); + expect(encryptionModules.encryptMessage).toBeCalledWith(mockPlainTextContent); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('UPDATE private_user_messages'), + [mockCipher, mockIV, mockTag, mockProps.messageId] + ); + expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1); + expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith( + expect.any(Object), + mockMessage.channel_id, + mockAuth.uid + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if there is an issue with the message', async () => { + const mockProps = { + messageId: 123, + content: {'mockContent' : 'mockContentValue'} + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + + expect(editMessage(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Message not found or cannot be edited'); + }); + + it('should throw if the message broadcast failed', async () => { + const mockProps = { + messageId: 123, + content: {'mockContent' : 'mockContentValue'} + }; + const mockMessage = { + channel_id: "mockChannelId" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockCipher = "mockCipherText"; + const mockIV = "mockIV"; + const mockTag = "mockTag"; + const mockEncryption = { + ciphertext: mockCipher, + iv: mockIV, + tag: mockTag + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); + (encryptionModules.encryptMessage as jest.Mock).mockReturnValue(mockEncryption); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.broadcastPrivateMessages as jest.Mock).mockRejectedValue(new Error('Broadcast Error')); + + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await editMessage(mockProps, mockAuth, mockReq); + expect(errorSpy).toBeCalledTimes(1); + expect(errorSpy).toBeCalledWith( + expect.stringContaining('broadcastPrivateMessages failed'), + expect.any(Error) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts new file mode 100644 index 00000000..70543408 --- /dev/null +++ b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts @@ -0,0 +1,55 @@ +jest.mock('shared/supabase/init'); + +import * as compatibililtyQuestionsModules from "api/get-compatibililty-questions"; +import * as supabaseInit from "shared/supabase/init"; + +describe('getCompatibilityQuestions', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + manyOrNone: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('when given valid input', () => { + it('should get compatibility questions', async () => { + const mockProps = {} as any; + const mockAuth = {} as any; + const mockReq = {} as any; + const mockQuestions = { + answer_type: "mockAnswerTypes", + category: "mockCategory", + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + id: "mockId", + importance_score: 123, + multiple_choice_options: {"mockChoice" : "mockChoiceValue"}, + question: "mockQuestion", + answer_count: 10, + score: 20 + }; + + (mockPg.manyOrNone as jest.Mock).mockResolvedValue(mockQuestions); + + const results: any = await compatibililtyQuestionsModules.getCompatibilityQuestions(mockProps, mockAuth, mockReq); + const [sql, params] = (mockPg.manyOrNone as jest.Mock).mock.calls[0]; + + expect(results.status).toBe('success'); + expect(results.questions).toBe(mockQuestions); + expect(sql).toEqual( + expect.stringContaining('compatibility_prompts.*') + ); + expect(sql).toEqual( + expect.stringContaining('COUNT(compatibility_answers.question_id) as answer_count') + ); + expect(sql).toEqual( + expect.stringContaining('AVG(POWER(compatibility_answers.importance + 1 + CASE WHEN compatibility_answers.explanation IS NULL THEN 1 ELSE 0 END, 2)) as score') + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-current-private-users.unit.test.ts b/backend/api/tests/unit/get-current-private-users.unit.test.ts new file mode 100644 index 00000000..eefd6254 --- /dev/null +++ b/backend/api/tests/unit/get-current-private-users.unit.test.ts @@ -0,0 +1,74 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/util/try-catch'); + +import { getCurrentPrivateUser } from "api/get-current-private-user"; +import * as supabaseInit from "shared/supabase/init"; +import { tryCatch } from "common/util/try-catch"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('getCurrentPrivateUser', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should get current private user', async () => { + const mockAuth = { uid: '321' } as AuthedUser; + const mockProps = {} as any; + const mockReq = {} as any; + const mockData = { + data: {"mockData" : "mockDataValue"}, + id: "mockId" + }; + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}); + + const result = await getCurrentPrivateUser(mockProps, mockAuth, mockReq); + + expect(result).toBe(mockData.data); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select * from private_users where id = $1'), + [mockAuth.uid] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if unable to get users private data', async () => { + const mockAuth = { uid: '321' } as AuthedUser; + const mockProps = {} as any; + const mockReq = {} as any; + const mockData = { + data: {"mockData" : "mockDataValue"}, + id: "mockId" + }; + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: Error}); + + expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Error fetching private user data: '); + }); + + it('should throw if unable to find user account', async () => { + const mockAuth = { uid: '321' } as AuthedUser; + const mockProps = {} as any; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: null, error: null}); + + expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Your account was not found'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-likes-and-ships.unit.test.ts b/backend/api/tests/unit/get-likes-and-ships.unit.test.ts new file mode 100644 index 00000000..d0c7341c --- /dev/null +++ b/backend/api/tests/unit/get-likes-and-ships.unit.test.ts @@ -0,0 +1,82 @@ +jest.mock('shared/supabase/init'); + +import * as likesAndShips from "api/get-likes-and-ships"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('getLikesAndShips', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + map: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should get all likes recieved/given an any ships', async () => { + const mockProps = {userId: "mockUserId"}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockLikesGiven = { + user_id: "mockUser_Id_likes_given", + created_Time: 123 + }; + const mockLikesReceived = { + user_id: "mockUser_Id_likes_received", + created_Time: 1234 + }; + const mockShips = { + creator_id: "mockCreatorId", + target_id: "mockTargetId", + target1_id: "mockTarget1Id", + target2_id: "mockTarget2Id", + target3_id: "mockTarget3Id", + created_time: 12345 + }; + + jest.spyOn(likesAndShips, 'getLikesAndShipsMain'); + (mockPg.map as jest.Mock) + .mockResolvedValueOnce(mockLikesGiven) + .mockResolvedValueOnce(mockLikesReceived) + .mockResolvedValueOnce(mockShips); + + + const result: any = await likesAndShips.getLikesAndShips(mockProps, mockAuth, mockReq); + const [sql1, params1, fn1] = (mockPg.map as jest.Mock).mock.calls[0]; + const [sql2, params2, fn2] = (mockPg.map as jest.Mock).mock.calls[1]; + const [sql3, params3, fn3] = (mockPg.map as jest.Mock).mock.calls[2]; + + expect(result.status).toBe('success'); + expect(result.likesGiven).toBe(mockLikesGiven); + expect(result.likesReceived).toBe(mockLikesReceived); + expect(result.ships).toBe(mockShips); + + expect(likesAndShips.getLikesAndShipsMain).toBeCalledTimes(1); + expect(likesAndShips.getLikesAndShipsMain).toBeCalledWith(mockProps.userId); + expect(mockPg.map).toHaveBeenNthCalledWith( + 1, + expect.stringContaining(sql1), + [mockProps.userId], + expect.any(Function) + ); + expect(mockPg.map).toHaveBeenNthCalledWith( + 2, + expect.stringContaining(sql2), + [mockProps.userId], + expect.any(Function) + ); + expect(mockPg.map).toHaveBeenNthCalledWith( + 3, + expect.stringContaining(sql3), + [mockProps.userId], + expect.any(Function) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-me.unit.test.ts b/backend/api/tests/unit/get-me.unit.test.ts new file mode 100644 index 00000000..a2d1f175 --- /dev/null +++ b/backend/api/tests/unit/get-me.unit.test.ts @@ -0,0 +1,29 @@ +jest.mock('api/get-user'); + +import { getMe } from "api/get-me"; +import { getUser } from "api/get-user"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('getMe', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should get the user', async () => { + const mockProps = {}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (getUser as jest.Mock).mockResolvedValue(null); + + await getMe(mockProps, mockAuth, mockReq); + + expect(getUser).toBeCalledTimes(1); + expect(getUser).toBeCalledWith({id: mockAuth.uid}); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-messages-count.unit.test.ts b/backend/api/tests/unit/get-messages-count.unit.test.ts new file mode 100644 index 00000000..9df56b80 --- /dev/null +++ b/backend/api/tests/unit/get-messages-count.unit.test.ts @@ -0,0 +1,40 @@ +jest.mock('shared/supabase/init'); + +import { getMessagesCount } from "api/get-messages-count"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('getMessagesCount', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + one: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should get message count', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockResults = { count: "10"}; + + (mockPg.one as jest.Mock).mockResolvedValue(mockResults); + + const result: any = await getMessagesCount(mockProps, mockAuth, mockReq); + + expect(result.count).toBe(Number(mockResults.count)); + expect(mockPg.one).toBeCalledTimes(1); + expect(mockPg.one).toBeCalledWith( + expect.stringContaining('SELECT COUNT(*) AS count'), + expect.any(Object) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-notifications.unit.test.ts b/backend/api/tests/unit/get-notifications.unit.test.ts new file mode 100644 index 00000000..5db82875 --- /dev/null +++ b/backend/api/tests/unit/get-notifications.unit.test.ts @@ -0,0 +1,44 @@ +jest.mock('shared/supabase/init'); + +import { getNotifications } from "api/get-notifications"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('getNotifications', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + map: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should user notifications', async () => { + const mockProps = { + limit: 10, + after: 2 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockNotifications = {} as any; + + (mockPg.map as jest.Mock).mockResolvedValue(mockNotifications); + + const result = await getNotifications(mockProps, mockAuth, mockReq); + + expect(result).toBe(mockNotifications); + expect(mockPg.map).toBeCalledTimes(1); + expect(mockPg.map).toBeCalledWith( + expect.stringContaining('select data from user_notifications'), + [mockAuth.uid, mockProps.limit, mockProps.after], + expect.any(Function) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-options.unit.test.ts b/backend/api/tests/unit/get-options.unit.test.ts new file mode 100644 index 00000000..52c262f1 --- /dev/null +++ b/backend/api/tests/unit/get-options.unit.test.ts @@ -0,0 +1,76 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/util/try-catch'); + +import { getOptions } from "api/get-options"; +import * as supabaseInit from "shared/supabase/init"; +import { tryCatch } from "common/util/try-catch"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('getOptions', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + manyOrNone: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return valid options', async () => { + const mockTable = "causes"; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = [ + { name: "mockName" }, + ]; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + (mockPg.manyOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}); + + const result: any = await getOptions({table: mockTable}, mockAuth, mockReq); + + expect(result.names).toContain(mockData[0].name); + expect(mockPg.manyOrNone).toBeCalledTimes(1); + expect(mockPg.manyOrNone).toBeCalledWith( + expect.stringContaining('SELECT interests.name') + ); + expect(tryCatch).toBeCalledTimes(1); + }); + }); + describe('when an error occurs', () => { + it('should throw if the table is invalid', async () => { + const mockTable = "causes"; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(false); + + expect(getOptions({table: mockTable}, mockAuth, mockReq)) + .rejects + .toThrow('Invalid table'); + }); + + it('should throw if unable to get profile options', async () => { + const mockTable = "causes"; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = [ + { name: "mockName" }, + ]; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + (mockPg.manyOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + + expect(getOptions({table: mockTable}, mockAuth, mockReq)) + .rejects + .toThrow('Error getting profile options'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-private-messages.unit.test.ts b/backend/api/tests/unit/get-private-messages.unit.test.ts new file mode 100644 index 00000000..ff6c3e5e --- /dev/null +++ b/backend/api/tests/unit/get-private-messages.unit.test.ts @@ -0,0 +1,288 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/messages'); + +import * as getPrivateMessages from "api/get-private-messages"; +import * as supabaseInit from "shared/supabase/init"; +import { tryCatch } from "common/util/try-catch"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('getChannelMemberships', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + map: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return channel memberships', async () => { + const mockProps = { + limit: 10, + channelId: 1, + createdTime: "mockCreatedTime", + lastUpdatedTime: "mockLastUpdatedTime" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockChannels = [ + { + channel_id: 123, + notify_after_time: "mockNotifyAfterTime", + created_time: "mockCreatedTime", + last_updated_time: "mockLastUpdatedTime" + } + ]; + const mockMembers = [ + { + channel_id: 1234, + user_id: "mockUserId" + } + ]; + (mockPg.map as jest.Mock) + .mockResolvedValueOnce(mockChannels) + .mockResolvedValueOnce(mockMembers); + + const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq); + + expect(results.channels).toBe(mockChannels); + expect(Object.keys(results.memberIdsByChannelId)[0]).toBe(String(mockMembers[0].channel_id)); + expect(Object.values(results.memberIdsByChannelId)[0]).toContain(mockMembers[0].user_id); + + expect(mockPg.map).toBeCalledTimes(2); + expect(mockPg.map).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('select channel_id, notify_after_time, pumcm.created_time, last_updated_time'), + [mockAuth.uid, mockProps.channelId, mockProps.limit], + expect.any(Function) + ); + expect(mockPg.map).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('select channel_id, user_id'), + [mockAuth.uid, [mockChannels[0].channel_id]], + expect.any(Function) + ); + }); + + it('should return channel memberships if there is no channelId', async () => { + const mockProps = { + limit: 10, + createdTime: "mockCreatedTime", + lastUpdatedTime: "mockLastUpdatedTime" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockChannels = [ + { + channel_id: 123, + notify_after_time: "mockNotifyAfterTime", + created_time: "mockCreatedTime", + last_updated_time: "mockLastUpdatedTime" + } + ]; + const mockMembers = [ + { + channel_id: 1234, + user_id: "mockUserId" + } + ]; + (mockPg.map as jest.Mock) + .mockResolvedValueOnce(mockChannels) + .mockResolvedValueOnce(mockMembers); + + const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq); + + expect(results.channels).toBe(mockChannels); + expect(Object.keys(results.memberIdsByChannelId)[0]).toBe(String(mockMembers[0].channel_id)); + expect(Object.values(results.memberIdsByChannelId)[0]).toContain(mockMembers[0].user_id); + + expect(mockPg.map).toBeCalledTimes(2); + expect(mockPg.map).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('with latest_channels as (select distinct on (pumc.id) pumc.id as channel_id'), + [mockAuth.uid, mockProps.createdTime, mockProps.limit, mockProps.lastUpdatedTime], + expect.any(Function) + ); + expect(mockPg.map).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('select channel_id, user_id'), + [mockAuth.uid, [mockChannels[0].channel_id]], + expect.any(Function) + ); + }); + + it('should return nothing if there are no channels', async () => { + const mockProps = { + limit: 10, + channelId: 1, + createdTime: "mockCreatedTime", + lastUpdatedTime: "mockLastUpdatedTime" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.map as jest.Mock).mockResolvedValueOnce(null); + + const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq); + + console.log(results); + + expect(results).toStrictEqual({ channels: [], memberIdsByChannelId: {} }); + + expect(mockPg.map).toBeCalledTimes(1); + expect(mockPg.map).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('select channel_id, notify_after_time, pumcm.created_time, last_updated_time'), + [mockAuth.uid, mockProps.channelId, mockProps.limit], + expect.any(Function) + ); + }); + }); +}); + +describe('getChannelMessagesEndpoint', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + map: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return the channel messages endpoint', async () => { + const mockProps = { + limit: 10, + channelId: 1, + id: 123 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = ['mockResult'] as any; + + (mockPg.map as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}); + + const result = await getPrivateMessages.getChannelMessagesEndpoint(mockProps, mockAuth, mockReq); + + expect(result).toBe(mockData); + expect(mockPg.map).toBeCalledTimes(1); + expect(mockPg.map).toBeCalledWith( + expect.stringContaining('select *, created_time as created_time_ts'), + [mockProps.channelId, mockAuth.uid, mockProps.limit, mockProps.id], + expect.any(Function) + ); + + }); + }); + describe('when an error occurs', () => { + it('should throw if unable to get messages', async () => { + const mockProps = { + limit: 10, + channelId: 1, + id: 123 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = ['mockResult'] as any; + + (mockPg.map as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + + expect(getPrivateMessages.getChannelMessagesEndpoint(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Error getting messages'); + }); + }); +}); + +describe('getLastSeenChannelTime', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + map: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return the last seen channel time', async () => { + const mockProps = { + channelIds: [ + 1, + 2, + 3, + ] + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUnseens = [ + [1, "mockString"] + ]; + + (mockPg.map as jest.Mock).mockResolvedValue(mockUnseens); + + const result = await getPrivateMessages.getLastSeenChannelTime(mockProps, mockAuth, mockReq); + + expect(result).toBe(mockUnseens); + expect(mockPg.map).toBeCalledTimes(1); + expect(mockPg.map).toBeCalledWith( + expect.stringContaining('select distinct on (channel_id) channel_id, created_time'), + [mockProps.channelIds, mockAuth.uid], + expect.any(Function) + ); + + }); + }); +}); + +describe('setChannelLastSeenTime', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + none: jest.fn(), + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should set channel last seen time', async () => { + const mockProps = { + channelId: 1 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + + await getPrivateMessages.setChannelLastSeenTime(mockProps, mockAuth, mockReq); + + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('insert into private_user_seen_message_channels (user_id, channel_id)'), + [mockAuth.uid, mockProps.channelId] + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-profile-answers.unit.test.ts b/backend/api/tests/unit/get-profile-answers.unit.test.ts new file mode 100644 index 00000000..9937acbe --- /dev/null +++ b/backend/api/tests/unit/get-profile-answers.unit.test.ts @@ -0,0 +1,53 @@ +jest.mock('shared/supabase/init'); + +import { getProfileAnswers } from "api/get-profile-answers"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('getProfileAnswers', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + manyOrNone: jest.fn() + }; + + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should get the answers for the userId', async () => { + const mockProps = { userId: "mockUserId" }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockAnswers = [ + { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + explanation: "mockExplanation", + id: 123, + importance: 10, + multiple_choice: 1234, + pref_choices: [1, 2, 3], + question_id: 12345 + } + ]; + + (mockPg.manyOrNone as jest.Mock).mockResolvedValue(mockAnswers); + + const result: any = await getProfileAnswers(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(result.answers).toBe(mockAnswers); + expect(mockPg.manyOrNone).toBeCalledTimes(1); + expect(mockPg.manyOrNone).toBeCalledWith( + expect.stringContaining('select * from compatibility_answers'), + [mockProps.userId] + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/has-free-like.unit.test.ts b/backend/api/tests/unit/has-free-like.unit.test.ts new file mode 100644 index 00000000..88018570 --- /dev/null +++ b/backend/api/tests/unit/has-free-like.unit.test.ts @@ -0,0 +1,57 @@ +jest.mock('shared/supabase/init'); + +import * as freeLikeModule from "api/has-free-like"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('hasFreeLike', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn() + }; + + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return if the user has a free like', async () => { + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockProps = {} as any; + + jest.spyOn( freeLikeModule, 'getHasFreeLike'); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(result.hasFreeLike).toBeTruthy(); + expect(freeLikeModule.getHasFreeLike).toBeCalledTimes(1); + expect(freeLikeModule.getHasFreeLike).toBeCalledWith(mockAuth.uid); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('from profile_likes'), + [mockAuth.uid] + ); + }); + + it('should return if the user does not have a free like', async () => { + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockProps = {} as any; + + jest.spyOn( freeLikeModule, 'getHasFreeLike'); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(true); + + const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq); + + expect(result.hasFreeLike).toBeFalsy(); + }); + }); +}); diff --git a/backend/api/tests/unit/health-unit.test.ts b/backend/api/tests/unit/health-unit.test.ts new file mode 100644 index 00000000..cc0fe452 --- /dev/null +++ b/backend/api/tests/unit/health-unit.test.ts @@ -0,0 +1,16 @@ +import { health } from "api/health"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('health', () => { + describe('when given valid input', () => { + it('should return the servers status(Health)', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + const result: any = await health(mockProps, mockAuth, mockReq); + expect(result.message).toBe('Server is working.'); + expect(result.uid).toBe(mockAuth.uid); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/hide-comment.unit.test.ts b/backend/api/tests/unit/hide-comment.unit.test.ts new file mode 100644 index 00000000..b9f02ccc --- /dev/null +++ b/backend/api/tests/unit/hide-comment.unit.test.ts @@ -0,0 +1,175 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/supabase/comment'); +jest.mock('shared/websockets/helpers'); + +import { hideComment } from "api/hide-comment"; +import * as supabaseInit from "shared/supabase/init"; +import * as envConsts from "common/envs/constants"; +import { convertComment } from "common/supabase/comment"; +import * as websocketHelpers from "shared/websockets/helpers"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('hideComment', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn() + }; + + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should successfully hide the comment if the user is an admin', async () => { + const mockProps = { + commentId: "mockCommentId", + hide: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockComment = { + content: { "mockContent": "mockContentValue" }, + created_time: "mockCreatedTime", + hidden: false, + id: 123, + on_user_id: "4321", + reply_to_comment_id: null, + user_avatar_url: "mockAvatarUrl", + user_id: "4321", + user_name: "mockUserName", + user_username: "mockUserUsername", + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment); + jest.spyOn(envConsts, 'isAdminId').mockReturnValue(true); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (convertComment as jest.Mock).mockReturnValue(null); + (websocketHelpers.broadcastUpdatedComment as jest.Mock).mockReturnValue(null); + + await hideComment(mockProps, mockAuth, mockReq); + + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select * from profile_comments where id = $1'), + [mockProps.commentId] + ); + expect(envConsts.isAdminId).toBeCalledTimes(1); + expect(envConsts.isAdminId).toBeCalledWith(mockAuth.uid); + expect(convertComment).toBeCalledTimes(1); + expect(convertComment).toBeCalledWith(mockComment); + expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1); + expect(websocketHelpers.broadcastUpdatedComment).toBeCalledWith(null); + }); + + it('should successfully hide the comment if the user is the one who made the comment', async () => { + const mockProps = { + commentId: "mockCommentId", + hide: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockComment = { + content: { "mockContent": "mockContentValue" }, + created_time: "mockCreatedTime", + hidden: false, + id: 123, + on_user_id: "4321", + reply_to_comment_id: null, + user_avatar_url: "mockAvatarUrl", + user_id: "321", + user_name: "mockUserName", + user_username: "mockUserUsername", + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment); + jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (convertComment as jest.Mock).mockReturnValue(null); + (websocketHelpers.broadcastUpdatedComment as jest.Mock).mockReturnValue(null); + + await hideComment(mockProps, mockAuth, mockReq); + }); + + it('should successfully hide the comment if the user is the one who is being commented on', async () => { + const mockProps = { + commentId: "mockCommentId", + hide: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockComment = { + content: { "mockContent": "mockContentValue" }, + created_time: "mockCreatedTime", + hidden: false, + id: 123, + on_user_id: "321", + reply_to_comment_id: null, + user_avatar_url: "mockAvatarUrl", + user_id: "4321", + user_name: "mockUserName", + user_username: "mockUserUsername", + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment); + jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (convertComment as jest.Mock).mockReturnValue(null); + (websocketHelpers.broadcastUpdatedComment as jest.Mock).mockReturnValue(null); + + await hideComment(mockProps, mockAuth, mockReq); + }); + }); + describe('when an error occurs', () => { + it('should throw if the comment was not found', async () => { + const mockProps = { + commentId: "mockCommentId", + hide: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + + expect(hideComment(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Comment not found'); + }); + + it('should throw if the user is not an admin, the comments author or the one being commented on', async () => { + const mockProps = { + commentId: "mockCommentId", + hide: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockComment = { + content: { "mockContent": "mockContentValue" }, + created_time: "mockCreatedTime", + hidden: false, + id: 123, + on_user_id: "4321", + reply_to_comment_id: null, + user_avatar_url: "mockAvatarUrl", + user_id: "4321", + user_name: "mockUserName", + user_username: "mockUserUsername", + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment); + jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (convertComment as jest.Mock).mockReturnValue(null); + (websocketHelpers.broadcastUpdatedComment as jest.Mock).mockReturnValue(null); + + expect(hideComment(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('You are not allowed to hide this comment'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts new file mode 100644 index 00000000..8c8e09b9 --- /dev/null +++ b/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts @@ -0,0 +1,95 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/utils'); +jest.mock('api/helpers/private-messages'); + +import { leavePrivateUserMessageChannel } from "api/leave-private-user-message-channel"; +import * as supabaseInit from "shared/supabase/init"; +import * as sharedUtils from "shared/utils"; +import * as messageHelpers from "api/helpers/private-messages"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('leavePrivateUserMessageChannel', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should leave a private message channel', async () => { + const mockProps = { channelId: 123 }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUser = { name: "mockName" }; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(true); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.leaveChatContent as jest.Mock).mockReturnValue(null); + (messageHelpers.insertPrivateMessage as jest.Mock).mockResolvedValue(null); + + const results = await leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq); + + expect(results.status).toBe('success'); + expect(results.channelId).toBe(mockProps.channelId); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select status from private_user_message_channel_members'), + [mockProps.channelId, mockAuth.uid] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('update private_user_message_channel_members'), + [mockProps.channelId, mockAuth.uid] + ); + expect(messageHelpers.leaveChatContent).toBeCalledTimes(1); + expect(messageHelpers.leaveChatContent).toBeCalledWith(mockUser.name); + expect(messageHelpers.insertPrivateMessage).toBeCalledTimes(1); + expect(messageHelpers.insertPrivateMessage).toBeCalledWith( + null, + mockProps.channelId, + mockAuth.uid, + 'system_status', + expect.any(Object) + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if the account was not found', async () => { + const mockProps = { channelId: 123 }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + + expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Your account was not found'); + }); + + it('should throw if you are not a member', async () => { + const mockProps = { channelId: 123 }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUser = { name: "mockName" }; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + + expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('You are not authorized to post to this channel'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/like-profile.unit.test.ts b/backend/api/tests/unit/like-profile.unit.test.ts new file mode 100644 index 00000000..858f9101 --- /dev/null +++ b/backend/api/tests/unit/like-profile.unit.test.ts @@ -0,0 +1,194 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/create-profile-notification'); +jest.mock('api/has-free-like'); +jest.mock('common/util/try-catch'); + +import { likeProfile } from "api/like-profile"; +import * as supabaseInit from "shared/supabase/init"; +import * as profileNotifiction from "shared/create-profile-notification"; +import * as likeModules from "api/has-free-like"; +import { tryCatch } from "common/util/try-catch"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('likeProfile', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + oneOrNone: jest.fn(), + one: jest.fn(), + none: jest.fn() + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should like the selected profile', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + likeId: "mockLikeId", + target_id: "mockTargetId" + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: false}) + .mockResolvedValueOnce({data: mockData, error: null}); + (likeModules.getHasFreeLike as jest.Mock).mockResolvedValue(true); + (mockPg.one as jest.Mock).mockResolvedValue(null); + + const result: any = await likeProfile(mockProps, mockAuth, mockReq); + + expect(result.result.status).toBe('success'); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select * from profile_likes where creator_id = $1 and target_id = $2'), + [mockAuth.uid, mockProps.targetUserId] + ); + expect(tryCatch).toBeCalledTimes(2); + expect(mockPg.one).toBeCalledTimes(1); + expect(mockPg.one).toBeCalledWith( + expect.stringContaining('insert into profile_likes (creator_id, target_id) values ($1, $2) returning *'), + [mockAuth.uid, mockProps.targetUserId] + ); + + (profileNotifiction.createProfileLikeNotification as jest.Mock).mockResolvedValue(null); + + await result.continue(); + + expect(profileNotifiction.createProfileLikeNotification).toBeCalledTimes(1); + expect(profileNotifiction.createProfileLikeNotification).toBeCalledWith(mockData); + }); + + it('should do nothing if there is already a like', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: true}); + + const result: any = await likeProfile(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + }); + + it('should remove a like', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + likeId: "mockLikeId", + target_id: "mockTargetId" + }; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}); + + const result: any = await likeProfile(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('delete from profile_likes where creator_id = $1 and target_id = $2'), + [mockAuth.uid, mockProps.targetUserId] + ); + }); + }); + + describe('when an error occurs', () => { + it('should throw if failed to remove like', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + likeId: "mockLikeId", + target_id: "mockTargetId" + }; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: mockData, error: Error}); + + expect(likeProfile(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Failed to remove like: '); + }); + + it('should throw if user has already used their free like', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + likeId: "mockLikeId", + target_id: "mockTargetId" + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: false}) + .mockResolvedValueOnce({data: mockData, error: null}); + (likeModules.getHasFreeLike as jest.Mock).mockResolvedValue(false); + + + expect(likeProfile(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('You already liked someone today!'); + }); + + it('should throw if failed to add like', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + likeId: "mockLikeId", + target_id: "mockTargetId" + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: false}) + .mockResolvedValueOnce({data: mockData, error: Error}); + (likeModules.getHasFreeLike as jest.Mock).mockResolvedValue(true); + (mockPg.one as jest.Mock).mockResolvedValue(null); + + expect(likeProfile(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Failed to add like: '); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts b/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts new file mode 100644 index 00000000..0f4e41a6 --- /dev/null +++ b/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts @@ -0,0 +1,39 @@ +jest.mock('shared/supabase/init'); + +import { markAllNotifsRead } from "api/mark-all-notifications-read"; +import { AuthedUser } from "api/helpers/endpoint"; +import * as supabaseInit from "shared/supabase/init"; + +describe('markAllNotifsRead', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + none: jest.fn() + }; + + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should mark all notifications as read', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + + await markAllNotifsRead(mockProps, mockAuth, mockReq); + + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('update user_notifications'), + [mockAuth.uid] + ); + }); + }); +}); \ No newline at end of file