diff --git a/backend/api/tests/unit/ban-user.unit.test.ts b/backend/api/tests/unit/ban-user.unit.test.ts index 4b4cd65..9732476 100644 --- a/backend/api/tests/unit/ban-user.unit.test.ts +++ b/backend/api/tests/unit/ban-user.unit.test.ts @@ -11,8 +11,7 @@ import { throwErrorIfNotMod } from "shared/helpers/auth"; import * as constants from "common/envs/constants"; import * as supabaseUsers from "shared/supabase/users"; import * as sharedAnalytics from "shared/analytics"; -import { } from "shared/helpers/auth"; -import { APIError, AuthedUser } from "api/helpers/endpoint" +import { AuthedUser } from "api/helpers/endpoint" describe('banUser', () => { @@ -24,13 +23,13 @@ describe('banUser', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('ban a user successfully', async () => { + + describe('when given valid input', () => { + it('should ban a user successfully', async () => { const mockUser = { userId: '123', unban: false @@ -42,15 +41,25 @@ describe('banUser', () => { await banUser(mockUser, mockAuth, mockReq); + expect(throwErrorIfNotMod).toBeCalledTimes(1); expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid); + expect(constants.isAdminId).toBeCalledTimes(1); expect(constants.isAdminId).toBeCalledWith(mockUser.userId); - expect(sharedAnalytics.trackPublicEvent) - .toBeCalledWith(mockAuth.uid, 'ban user', {userId: mockUser.userId}); - expect(supabaseUsers.updateUser) - .toBeCalledWith(mockPg, mockUser.userId, {isBannedFromPosting: true}); + expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(1); + expect(sharedAnalytics.trackPublicEvent).toBeCalledWith( + mockAuth.uid, + 'ban user', + {userId: mockUser.userId} + ); + expect(supabaseUsers.updateUser).toBeCalledTimes(1); + expect(supabaseUsers.updateUser).toBeCalledWith( + mockPg, + mockUser.userId, + {isBannedFromPosting: true} + ); }); - it('unban a user successfully', async () => { + it('should unban a user successfully', async () => { const mockUser = { userId: '123', unban: true @@ -64,13 +73,20 @@ describe('banUser', () => { expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid); expect(constants.isAdminId).toBeCalledWith(mockUser.userId); - expect(sharedAnalytics.trackPublicEvent) - .toBeCalledWith(mockAuth.uid, 'ban user', {userId: mockUser.userId}); - expect(supabaseUsers.updateUser) - .toBeCalledWith(mockPg, mockUser.userId, {isBannedFromPosting: false}); + expect(sharedAnalytics.trackPublicEvent).toBeCalledWith( + mockAuth.uid, + 'ban user', + {userId: mockUser.userId} + ); + expect(supabaseUsers.updateUser).toBeCalledWith( + mockPg, + mockUser.userId, + {isBannedFromPosting: false} + ); }); - - it('throw and error if the ban requester is not a mod or admin', async () => { + }); + describe('when an error occurs', () => { + it('throw if the ban requester is not a mod or admin', async () => { const mockUser = { userId: '123', unban: false @@ -79,21 +95,16 @@ describe('banUser', () => { const mockReq = {} as any; (throwErrorIfNotMod as jest.Mock).mockRejectedValue( - new APIError( - 403, - `User ${mockAuth.uid} must be an admin or trusted to perform this action.` - ) + new Error(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`) ); await expect(banUser(mockUser, mockAuth, mockReq)) .rejects .toThrowError(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`); expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid); - expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(0); - expect(supabaseUsers.updateUser).toBeCalledTimes(0); }); - it('throw an error if the ban target is an admin', async () => { + it('throw if the ban target is an admin', async () => { const mockUser = { userId: '123', unban: false @@ -108,8 +119,6 @@ describe('banUser', () => { .toThrowError('Cannot ban admin'); expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid); expect(constants.isAdminId).toBeCalledWith(mockUser.userId); - expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(0); - expect(supabaseUsers.updateUser).toBeCalledTimes(0); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/block-user.unit.test.ts b/backend/api/tests/unit/block-user.unit.test.ts index f46ce95..87f19ed 100644 --- a/backend/api/tests/unit/block-user.unit.test.ts +++ b/backend/api/tests/unit/block-user.unit.test.ts @@ -23,37 +23,35 @@ describe('blockUser', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg) }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { + describe('when given valid input', () => { it('block the user successfully', async () => { const mockParams = { id: '123' } const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - (supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null); - await blockUserModule.blockUser(mockParams, mockAuth, mockReq) - expect(mockPg.tx).toHaveBeenCalledTimes(1) - - expect(supabaseUsers.updatePrivateUser) - .toHaveBeenCalledWith( - expect.any(Object), - mockAuth.uid, - { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)} - ); - expect(supabaseUsers.updatePrivateUser) - .toHaveBeenCalledWith( - expect.any(Object), - mockParams.id, - { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)} - ); + expect(mockPg.tx).toHaveBeenCalledTimes(1); + expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(2); + expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + mockAuth.uid, + { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)} + ); + expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + mockParams.id, + { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)} + ); }); - + }); + describe('when an error occurs', () => { it('throw an error if the user tries to block themselves', async () => { const mockParams = { id: '123' } const mockAuth = {uid: '123'} as AuthedUser; @@ -61,12 +59,9 @@ describe('blockUser', () => { expect(blockUserModule.blockUser(mockParams, mockAuth, mockReq)) .rejects - .toThrowError('You cannot block yourself') - - expect(mockPg.tx).toHaveBeenCalledTimes(0) + .toThrowError('You cannot block yourself'); }); }); - }); describe('unblockUser', () => { @@ -84,35 +79,32 @@ describe('unblockUser', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg) }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('block the user successfully', async () => { + describe('when given valid input', () => { + it('should block the user successfully', async () => { const mockParams = { id: '123' } const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - (supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null); - await blockUserModule.unblockUser(mockParams, mockAuth, mockReq) - expect(mockPg.tx).toHaveBeenCalledTimes(1) - - expect(supabaseUsers.updatePrivateUser) - .toHaveBeenCalledWith( - expect.any(Object), - mockAuth.uid, - { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)} - ); - expect(supabaseUsers.updatePrivateUser) - .toHaveBeenCalledWith( - expect.any(Object), - mockParams.id, - { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)} - ); + expect(mockPg.tx).toHaveBeenCalledTimes(1); + expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(2); + expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + mockAuth.uid, + { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)} + ); + expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + mockParams.id, + { blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)} + ); }); }); diff --git a/backend/api/tests/unit/compatible-profiles.unit.test.ts b/backend/api/tests/unit/compatible-profiles.unit.test.ts index bc2ee24..4cfff7b 100644 --- a/backend/api/tests/unit/compatible-profiles.unit.test.ts +++ b/backend/api/tests/unit/compatible-profiles.unit.test.ts @@ -1,32 +1,41 @@ -import * as supabaseInit from "shared/supabase/init"; +jest.mock('shared/supabase/init'); + import {getCompatibleProfiles} from "api/compatible-profiles"; +import * as supabaseInit from "shared/supabase/init"; -jest.mock('shared/supabase/init') describe('getCompatibleProfiles', () => { + let mockPg = {} as any; beforeEach(() => { jest.resetAllMocks(); - const mockPg = { - none: jest.fn().mockResolvedValue(null), - one: jest.fn().mockResolvedValue(null), - oneOrNone: jest.fn().mockResolvedValue(null), - any: jest.fn().mockResolvedValue([]), - map: jest.fn().mockResolvedValue([["abc", {score: 0.69}]]), - } as any; + mockPg = { + map: jest.fn().mockResolvedValue([]), + }; (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('successfully get compatible profiles when supplied with a valid user Id', async () => { - const results = await getCompatibleProfiles("123"); + describe('when given valid input', () => { + it('should successfully get compatible profiles', async () => { + const mockProps = '123'; + const mockScores = ["abc", { score: 0.69 }]; + const mockScoresFromEntries = {"abc": { score: 0.69 }}; + + (mockPg.map as jest.Mock).mockResolvedValue([mockScores]); + + const results = await getCompatibleProfiles(mockProps); + const [sql, param, fn] = mockPg.map.mock.calls[0]; + expect(results.status).toEqual('success'); - expect(results.profileCompatibilityScores).toEqual({"abc": {score: 0.69}}); + expect(results.profileCompatibilityScores).toEqual(mockScoresFromEntries); + expect(mockPg.map).toBeCalledTimes(1); + expect(sql).toContain('select *'); + expect(sql).toContain('from compatibility_scores'); + expect(param).toStrictEqual([mockProps]); + expect(fn).toEqual(expect.any(Function)); }); - }); }); diff --git a/backend/api/tests/unit/contact.unit.test.ts b/backend/api/tests/unit/contact.unit.test.ts index b134f5e..1719697 100644 --- a/backend/api/tests/unit/contact.unit.test.ts +++ b/backend/api/tests/unit/contact.unit.test.ts @@ -14,7 +14,6 @@ describe('contact', () => { let mockPg: any; beforeEach(() => { jest.resetAllMocks(); - mockPg = { oneOrNone: jest.fn(), }; @@ -22,13 +21,12 @@ describe('contact', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('send a discord message to the user', async () => { + describe('when given valid input', () => { + it('should send a discord message to the user', async () => { const mockProps = { content: { type: 'doc', @@ -52,29 +50,42 @@ describe('contact', () => { const mockReturnData = {} as any; (tryCatch as jest.Mock).mockResolvedValue({ data: mockReturnData, error: null }); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser); - (sendDiscordMessage as jest.Mock).mockResolvedValue(null); + const results = await contact(mockProps, mockAuth, mockReq); + + expect(results.success).toBe(true); + expect(results.result).toStrictEqual({}); + expect(tryCatch).toBeCalledTimes(1); expect(supabaseUtils.insert).toBeCalledTimes(1) expect(supabaseUtils.insert).toBeCalledWith( - mockPg, + expect.any(Object), 'contact', { user_id: mockProps.userId, content: JSON.stringify(mockProps.content) } ); - expect(results.success).toBe(true); + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser); + await results.continue(); + + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select name from users where id = $1'), + [mockProps.userId] + ); + expect(sendDiscordMessage).toBeCalledTimes(1); expect(sendDiscordMessage).toBeCalledWith( expect.stringContaining(`New message from ${mockDbUser.name}`), 'contact' - ) - expect(sendDiscordMessage).toBeCalledTimes(1); + ); }); - - it('throw an error if the inser function fails', async () => { + }); + + describe('when an error occurs', () => { + it('should throw if the insert function fails', async () => { const mockProps = { content: { type: 'doc', @@ -100,15 +111,59 @@ describe('contact', () => { expect(contact(mockProps, mockAuth, mockReq)) .rejects .toThrowError('Failed to submit contact message'); + }); + + it('should throw if unable to send discord message', async () => { + const mockProps = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Error test message' + } + ] + } + ] + }, + userId: '123' + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockDbUser = { name: 'Humphrey Mocker' }; + const mockReturnData = {} as any; + + (tryCatch as jest.Mock).mockResolvedValue({ data: mockReturnData, error: null }); + + const results = await contact(mockProps, mockAuth, mockReq); + + expect(results.success).toBe(true); + expect(results.result).toStrictEqual({}); + expect(tryCatch).toBeCalledTimes(1); expect(supabaseUtils.insert).toBeCalledTimes(1) expect(supabaseUtils.insert).toBeCalledWith( - mockPg, + expect.any(Object), 'contact', { user_id: mockProps.userId, content: JSON.stringify(mockProps.content) } ); + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser); + (sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Unable to send message')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await results.continue(); + + expect(errorSpy).toBeCalledTimes(1); + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to send discord contact'), + expect.objectContaining({name: 'Error'}) + ); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/create-bookmarked-search.unit.test.ts b/backend/api/tests/unit/create-bookmarked-search.unit.test.ts index 1108a6a..32bf332 100644 --- a/backend/api/tests/unit/create-bookmarked-search.unit.test.ts +++ b/backend/api/tests/unit/create-bookmarked-search.unit.test.ts @@ -8,7 +8,6 @@ describe('createBookmarkedSearch', () => { let mockPg: any; beforeEach(() => { jest.resetAllMocks(); - mockPg = { one: jest.fn(), }; @@ -16,13 +15,12 @@ describe('createBookmarkedSearch', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('insert a bookmarked search into the database', async () => { + describe('when given valid input', () => { + it('should insert a bookmarked search into the database', async () => { const mockProps = { search_filters: 'mock_search_filters', location: 'mock_location', @@ -30,9 +28,14 @@ describe('createBookmarkedSearch', () => { }; const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; + const mockInserted = "mockInsertedReturn"; + + (mockPg.one as jest.Mock).mockResolvedValue(mockInserted); + + const result = await createBookmarkedSearch(mockProps, mockAuth, mockReq); - await createBookmarkedSearch(mockProps, mockAuth, mockReq) - expect(mockPg.one).toBeCalledTimes(1) + expect(result).toBe(mockInserted); + expect(mockPg.one).toBeCalledTimes(1); expect(mockPg.one).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO bookmarked_searches'), [ diff --git a/backend/api/tests/unit/create-comment.unit.test.ts b/backend/api/tests/unit/create-comment.unit.test.ts index b7e7e1d..51ec44c 100644 --- a/backend/api/tests/unit/create-comment.unit.test.ts +++ b/backend/api/tests/unit/create-comment.unit.test.ts @@ -15,32 +15,26 @@ import * as supabaseNotifications from "shared/supabase/notifications"; import * as emailHelpers from "email/functions/helpers"; import * as websocketHelpers from "shared/websockets/helpers"; import { convertComment } from "common/supabase/comment"; +import { richTextToString } from "common/util/parse"; describe('createComment', () => { let mockPg: any; beforeEach(() => { jest.resetAllMocks(); - mockPg = { one: jest.fn() }; (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); - (supabaseNotifications.insertNotificationToSupabase as jest.Mock) - .mockResolvedValue(null); - (emailHelpers.sendNewEndorsementEmail as jest.Mock) - .mockResolvedValue(null); - (convertComment as jest.Mock) - .mockResolvedValue(null); }); afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('successfully create a comment with information provided', async () => { + describe('when given valid input', () => { + it('should successfully create a comment', async () => { const mockUserId = { userId: '123', blockedUserIds: ['111'] @@ -74,12 +68,17 @@ describe('createComment', () => { const mockReq = {} as any; const mockReplyToCommentId = {} as any; const mockComment = {id: 12}; - const mockNotificationDestination = {} as any; + const mockNotificationDestination = { + sendToBrowser: true, + sendToMobile: false, + sendToEmail: true + }; const mockProps = { userId: mockUserId.userId, content: mockContent.content, replyToCommentId: mockReplyToCommentId }; + const mockConvertCommentReturn = 'mockConverComment'; (sharedUtils.getUser as jest.Mock) .mockResolvedValueOnce(mockCreator) @@ -90,24 +89,51 @@ describe('createComment', () => { (mockPg.one as jest.Mock).mockResolvedValue(mockComment); (notificationPrefs.getNotificationDestinationsForUser as jest.Mock) .mockReturnValue(mockNotificationDestination); + (convertComment as jest.Mock).mockReturnValue(mockConvertCommentReturn); const results = await createComment(mockProps, mockAuth, mockReq); expect(results.status).toBe('success'); expect(sharedUtils.getUser).toBeCalledTimes(2); - expect(sharedUtils.getUser).toBeCalledWith(mockUserId.userId); - expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(sharedUtils.getUser).toHaveBeenNthCalledWith(1, mockAuth.uid); + expect(sharedUtils.getUser).toHaveBeenNthCalledWith(2, mockUserId.userId); expect(sharedUtils.getPrivateUser).toBeCalledTimes(2); + expect(sharedUtils.getPrivateUser).toHaveBeenNthCalledWith(1, mockProps.userId); + expect(sharedUtils.getPrivateUser).toHaveBeenNthCalledWith(2, mockOnUser.id); expect(mockPg.one).toBeCalledTimes(1); expect(mockPg.one).toBeCalledWith( expect.stringContaining('insert into profile_comments'), - expect.arrayContaining([mockCreator.id]) + [ + mockCreator.id, + mockCreator.name, + mockCreator.username, + mockCreator.avatarUrl, + mockProps.userId, + mockProps.content, + mockProps.replyToCommentId + ] ); - expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1) - + expect(notificationPrefs.getNotificationDestinationsForUser).toBeCalledTimes(1); + expect(notificationPrefs.getNotificationDestinationsForUser).toBeCalledWith(mockOnUser, 'new_endorsement'); + expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledTimes(1); + expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledWith( + expect.any(Object), + expect.any(Object) + ); + expect(emailHelpers.sendNewEndorsementEmail).toBeCalledTimes(1); + expect(emailHelpers.sendNewEndorsementEmail).toBeCalledWith( + mockOnUser, + mockCreator, + mockOnUser, + richTextToString(mockProps.content) + ); + expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1); + expect(websocketHelpers.broadcastUpdatedComment).toBeCalledWith(mockConvertCommentReturn); }); + }); - it('throw an error if there is no user matching the userId', async () => { + describe('when an error occurs', () => { + it('should throw if there is no user matching the userId', async () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockReplyToCommentId = {} as any; @@ -147,14 +173,16 @@ describe('createComment', () => { (sharedUtils.getUser as jest.Mock) .mockResolvedValueOnce(mockCreator) - .mockResolvedValueOnce(null); + .mockResolvedValueOnce(false); (sharedUtils.getPrivateUser as jest.Mock) .mockResolvedValue(mockUserId); - expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('User not found'); + expect(createComment( mockProps, mockAuth, mockReq )) + .rejects + .toThrowError('User not found'); }); - it('throw an error if there is no account associated with the authId', async () => { + it('throw if there is no account associated with the authId', async () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockReplyToCommentId = {} as any; @@ -188,10 +216,12 @@ describe('createComment', () => { (sharedUtils.getUser as jest.Mock) .mockResolvedValueOnce(null); - expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('Your account was not found'); + expect(createComment( mockProps, mockAuth, mockReq )) + .rejects + .toThrowError('Your account was not found'); }); - it('throw an error if the account is banned from posting', async () => { + it('throw if the account is banned from posting', async () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockReplyToCommentId = {} as any; @@ -232,10 +262,12 @@ describe('createComment', () => { (sharedUtils.getUser as jest.Mock) .mockResolvedValueOnce(mockCreator); - expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('You are banned'); + expect(createComment( mockProps, mockAuth, mockReq )) + .rejects + .toThrowError('You are banned'); }); - it('throw an error if the other user is not found', async () => { + it('throw if the other user is not found', async () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockReplyToCommentId = {} as any; @@ -278,10 +310,12 @@ describe('createComment', () => { (sharedUtils.getPrivateUser as jest.Mock) .mockResolvedValue(null); - expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('Other user not found'); + expect(createComment( mockProps, mockAuth, mockReq )) + .rejects + .toThrowError('Other user not found'); }); - it('throw an error if the user has blocked you', async () => { + it('throw if the user has blocked you', async () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockReplyToCommentId = {} as any; @@ -324,10 +358,12 @@ describe('createComment', () => { (sharedUtils.getPrivateUser as jest.Mock) .mockResolvedValue(mockUserId); - expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('User has blocked you'); + expect(createComment( mockProps, mockAuth, mockReq )) + .rejects + .toThrowError('User has blocked you'); }); - it('throw an error if the comment is too long', async () => { + it('throw if the comment is too long', async () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockReplyToCommentId = {} as any; @@ -369,9 +405,10 @@ describe('createComment', () => { .mockResolvedValueOnce(mockCreator); (sharedUtils.getPrivateUser as jest.Mock) .mockResolvedValue(mockUserId); - console.log(JSON.stringify(mockContent.content).length); - expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('Comment is too long'); + expect(createComment( mockProps, mockAuth, mockReq )) + .rejects + .toThrowError('Comment is too long'); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/create-compatibility-question.unit.test.ts b/backend/api/tests/unit/create-compatibility-question.unit.test.ts index 3276d3d..70a69dd 100644 --- a/backend/api/tests/unit/create-compatibility-question.unit.test.ts +++ b/backend/api/tests/unit/create-compatibility-question.unit.test.ts @@ -18,12 +18,12 @@ describe('createCompatibilityQuestion', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('successfully create compatibility questions', async () => { + + describe('when given valid input', () => { + it('should successfully create compatibility questions', async () => { const mockQuestion = {} as any; const mockOptions = {} as any; const mockProps = {options:mockOptions, question:mockQuestion}; @@ -41,31 +41,45 @@ describe('createCompatibilityQuestion', () => { multiple_choice_options: {"first_choice":"first_answer"}, question: "mockQuestion" }; + (shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); - (supabaseUtils.insert as jest.Mock).mockResolvedValue(mockData); (tryCatch as jest.Mock).mockResolvedValue({data:mockData, error: null}); const results = await createCompatibilityQuestion(mockProps, mockAuth, mockReq); expect(results.question).toEqual(mockData); - + expect(shareUtils.getUser).toBeCalledTimes(1); + expect(shareUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(supabaseUtils.insert).toBeCalledTimes(1); + expect(supabaseUtils.insert).toBeCalledWith( + expect.any(Object), + 'compatibility_prompts', + { + creator_id: mockCreator.id, + question: mockQuestion, + answer_type: 'compatibility_multiple_choice', + multiple_choice_options: mockOptions + } + ); }); - - it('throws an error if the account does not exist', async () => { + }); + describe('when an error occurs', () => { + it('throws if the account does not exist', async () => { const mockQuestion = {} as any; const mockOptions = {} as any; const mockProps = {options:mockOptions, question:mockQuestion}; const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - (shareUtils.getUser as jest.Mock).mockResolvedValue(null); + + (shareUtils.getUser as jest.Mock).mockResolvedValue(false); expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq)) .rejects - .toThrowError('Your account was not found') + .toThrowError('Your account was not found'); }); - it('throws an error if unable to create the question', async () => { + it('throws if unable to create the question', async () => { const mockQuestion = {} as any; const mockOptions = {} as any; const mockProps = {options:mockOptions, question:mockQuestion}; @@ -74,23 +88,13 @@ describe('createCompatibilityQuestion', () => { const mockCreator = { id: '123', }; - const mockData = { - answer_type: "mockAnswerType", - category: "mockCategory", - created_time: "mockCreatedTime", - id: 1, - importance_score: 1, - multiple_choice_options: {"first_choice":"first_answer"}, - question: "mockQuestion" - }; + (shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); - (supabaseUtils.insert as jest.Mock).mockResolvedValue(mockData); (tryCatch as jest.Mock).mockResolvedValue({data:null, error: Error}); expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq)) .rejects - .toThrowError('Error creating question') - + .toThrowError('Error creating question'); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/create-notification.unit.test.ts b/backend/api/tests/unit/create-notification.unit.test.ts index e979151..1418078 100644 --- a/backend/api/tests/unit/create-notification.unit.test.ts +++ b/backend/api/tests/unit/create-notification.unit.test.ts @@ -8,25 +8,23 @@ import { tryCatch } from "common/util/try-catch"; import * as supabaseNotifications from "shared/supabase/notifications"; import { Notification } from "common/notifications"; -type MockNotificationUser = Pick; - describe('createNotifications', () => { + let mockPg = {} as any; beforeEach(() => { jest.resetAllMocks(); - const mockPg = { + mockPg = { many: jest.fn().mockReturnValue(null) - } as any; + }; (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('sucessfully create a notification', async () => { + describe('when given valid input', () => { + it('should sucessfully create a notification', async () => { const mockUsers = [ { created_time: "mockCreatedTime", @@ -39,40 +37,63 @@ describe('createNotifications', () => { ]; const mockNotification = { userId: "mockUserId" - } as MockNotificationUser; + } as Notification; (tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null}); - (supabaseNotifications.insertNotificationToSupabase as jest.Mock) - .mockResolvedValue(null); + jest.spyOn(createNotificationModules, 'createNotification'); + + const results = await createNotificationModules.createNotifications(mockNotification); - const results = await createNotificationModules.createNotifications(mockNotification as Notification); expect(results?.success).toBeTruthy; + expect(tryCatch).toBeCalledTimes(1); + expect(mockPg.many).toBeCalledTimes(1); + expect(mockPg.many).toBeCalledWith('select * from users'); + expect(createNotificationModules.createNotification).toBeCalledTimes(1); + expect(createNotificationModules.createNotification).toBeCalledWith( + mockUsers[0], + mockNotification, + expect.any(Object) + ); + expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledTimes(1); + expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledWith( + mockNotification, + expect.any(Object) + ); }); + }); - it('throws an error if its unable to fetch users', async () => { - const mockUsers = [ - { - created_time: "mockCreatedTime", - data: {"mockData": "mockDataJson"}, - id: "mockId", - name: "mockName", - name_user_vector: "mockNUV", - username: "mockUsername" - }, - ]; + describe('when an error occurs', () => { + it('should throw if its unable to fetch users', async () => { const mockNotification = { userId: "mockUserId" - } as MockNotificationUser; + } as Notification; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - (tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:Error}); + (tryCatch as jest.Mock).mockResolvedValue({data: null, error:Error}); - await createNotificationModules.createNotifications(mockNotification as Notification) - expect(errorSpy).toHaveBeenCalledWith('Error fetching users', expect.objectContaining({name: 'Error'})) + await createNotificationModules.createNotifications(mockNotification); + + expect(errorSpy).toBeCalledWith( + 'Error fetching users', + expect.objectContaining({name: 'Error'}) + ); }); - it('throws an error if there are no users', async () => { + it('should throw if there are no users', async () => { + const mockNotification = { + userId: "mockUserId" + } as Notification; + + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + (tryCatch as jest.Mock).mockResolvedValue({data: null, error:null}); + + await createNotificationModules.createNotifications(mockNotification); + expect(errorSpy).toBeCalledWith('No users found'); + }); + + it('should throw if unable to create notification', async () => { const mockUsers = [ { created_time: "mockCreatedTime", @@ -85,14 +106,20 @@ describe('createNotifications', () => { ]; const mockNotification = { userId: "mockUserId" - } as MockNotificationUser; + } as Notification; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - (tryCatch as jest.Mock).mockResolvedValue({data: null, error:null}); + (tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null}); + jest.spyOn(createNotificationModules, 'createNotification').mockRejectedValue(new Error('Creation failure')); - await createNotificationModules.createNotifications(mockNotification as Notification) - expect(errorSpy).toHaveBeenCalledWith('No users found') + await createNotificationModules.createNotifications(mockNotification); + + expect(errorSpy).toBeCalledWith( + 'Failed to create notification', + expect.objectContaining({name: 'Error'}), + mockUsers[0] + ); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts index 5c3d42f..0d392f4 100644 --- a/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts +++ b/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts @@ -23,13 +23,12 @@ describe('createPrivateUserMessageChannel', () => { (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg) }); - afterEach(() => { jest.restoreAllMocks() }); - describe('should', () => { - it('successfully create a private user message channel (currentChannel)', async () => { + describe('when given valid input', () => { + it('should successfully create a private user message channel (currentChannel)', async () => { const mockBody = { userIds: ["123"] }; @@ -55,29 +54,27 @@ describe('createPrivateUserMessageChannel', () => { isBannedFromPosting: false }; - (sharedUtils.getUser as jest.Mock) - .mockResolvedValue(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserIds); - (utilArrayModules.filterDefined as jest.Mock) - .mockReturnValue(mockPrivateUsers); - (mockPg.oneOrNone as jest.Mock) - .mockResolvedValue(mockCurrentChannel); - (privateMessageModules.addUsersToPrivateMessageChannel as jest.Mock) - .mockResolvedValue(null); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); + (utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockCurrentChannel); const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq); + + expect(results.status).toBe('success'); + expect(results.channelId).toBe(444); expect(sharedUtils.getUser).toBeCalledTimes(1); expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); expect(sharedUtils.getPrivateUser).toBeCalledTimes(2); expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]); expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]); - expect(results.status).toBe('success'); - expect(results.channelId).toBe(444) - + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select channel_id from private_user_message_channel_members'), + [mockUserIds] + ); }); - it('successfully create a private user message channel (channel)', async () => { + it('should successfully create a private user message channel (channel)', async () => { const mockBody = { userIds: ["123"] }; @@ -103,45 +100,54 @@ describe('createPrivateUserMessageChannel', () => { isBannedFromPosting: false }; - (sharedUtils.getUser as jest.Mock) - .mockResolvedValue(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserIds); - (utilArrayModules.filterDefined as jest.Mock) - .mockReturnValue(mockPrivateUsers); - (mockPg.oneOrNone as jest.Mock) - .mockResolvedValue(null); - (mockPg.one as jest.Mock) - .mockResolvedValue(mockChannel); - (privateMessageModules.addUsersToPrivateMessageChannel as jest.Mock) - .mockResolvedValue(null); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); + (utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + (mockPg.one as jest.Mock).mockResolvedValue(mockChannel); const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq); + + expect(results.status).toBe('success'); + expect(results.channelId).toBe(333); expect(sharedUtils.getUser).toBeCalledTimes(1); expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); expect(sharedUtils.getPrivateUser).toBeCalledTimes(2); expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]); expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]); - expect(results.status).toBe('success'); - expect(results.channelId).toBe(333) - + expect(mockPg.one).toBeCalledTimes(1); + expect(mockPg.one).toBeCalledWith( + expect.stringContaining('insert into private_user_message_channels default values returning id') + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('insert into private_user_message_channel_members (channel_id, user_id, role, status)'), + [mockChannel.id, mockAuth.uid] + ); + expect(privateMessageModules.addUsersToPrivateMessageChannel).toBeCalledTimes(1); + expect(privateMessageModules.addUsersToPrivateMessageChannel).toBeCalledWith( + [mockUserIds[0]], + mockChannel.id, + expect.any(Object) + ); }); - - it('throw an error if the user account doesnt exist', async () => { + }); + + describe('when an error occurs', () => { + it('should throw if the user account doesnt exist', async () => { const mockBody = { userIds: ["123"] }; const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - (sharedUtils.getUser as jest.Mock) - .mockResolvedValue(null); + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) .rejects .toThrowError('Your account was not found'); }); - it('throw an error if the authId is banned from posting', async () => { + it('should throw if the authId is banned from posting', async () => { const mockBody = { userIds: ["123"] }; @@ -151,21 +157,17 @@ describe('createPrivateUserMessageChannel', () => { isBannedFromPosting: true }; - (sharedUtils.getUser as jest.Mock) - .mockResolvedValue(mockCreator); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) .rejects .toThrowError('You are banned'); - expect(sharedUtils.getUser).toBeCalledTimes(1); - expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); }); - it('throw an error if the array lengths dont match (privateUsers, userIds)', async () => { + it('should throw if the array lengths dont match (privateUsers, userIds)', async () => { const mockBody = { userIds: ["123"] }; - const mockUserIds = ['123']; const mockPrivateUsers = [ { id: '123', @@ -181,8 +183,6 @@ describe('createPrivateUserMessageChannel', () => { (sharedUtils.getUser as jest.Mock) .mockResolvedValue(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserIds); (utilArrayModules.filterDefined as jest.Mock) .mockReturnValue(mockPrivateUsers); @@ -191,11 +191,10 @@ describe('createPrivateUserMessageChannel', () => { .toThrowError(`Private user ${mockAuth.uid} not found`); }); - it('throw an error if there is a blocked user in the userId list', async () => { + it('should throw if there is a blocked user in the userId list', async () => { const mockBody = { userIds: ["123"] }; - const mockUserIds = ['321']; const mockPrivateUsers = [ { id: '123', @@ -216,8 +215,6 @@ describe('createPrivateUserMessageChannel', () => { (sharedUtils.getUser as jest.Mock) .mockResolvedValue(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserIds); (utilArrayModules.filterDefined as jest.Mock) .mockReturnValue(mockPrivateUsers); diff --git a/backend/api/tests/unit/create-private-user-message.unit.test.ts b/backend/api/tests/unit/create-private-user-message.unit.test.ts index bbea013..ca68f40 100644 --- a/backend/api/tests/unit/create-private-user-message.unit.test.ts +++ b/backend/api/tests/unit/create-private-user-message.unit.test.ts @@ -23,7 +23,7 @@ describe('createPrivateUserMessage', () => { jest.restoreAllMocks(); }); - describe('should', () => { + describe('when given valid input', () => { it('successfully create a private user message', async () => { const mockBody = { content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH-8))}, @@ -36,10 +36,12 @@ describe('createPrivateUserMessage', () => { }; (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); - (helpersPrivateMessagesModules.createPrivateUserMessageMain as jest.Mock) - .mockResolvedValue(null); await createPrivateUserMessage(mockBody, mockAuth, mockReq); + + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledTimes(1); expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledWith( mockCreator, mockBody.channelId, @@ -48,8 +50,9 @@ describe('createPrivateUserMessage', () => { 'private' ); }); - - it('throw an error if the content is too long', async () => { + }); + describe('when an error occurs', () => { + it('should throw if the content is too long', async () => { const mockBody = { content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH))}, channelId: 123 @@ -62,7 +65,7 @@ describe('createPrivateUserMessage', () => { .toThrowError(`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`); }); - it('throw an error if the user does not exist', async () => { + it('should throw if the user does not exist', async () => { const mockBody = { content: {"mockJson": "mockJsonContent"}, channelId: 123 @@ -70,14 +73,14 @@ describe('createPrivateUserMessage', () => { const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)) .rejects .toThrowError(`Your account was not found`); }); - it('throw an error if the user does not exist', async () => { + it('should throw if the user does not exist', async () => { const mockBody = { content: {"mockJson": "mockJsonContent"}, channelId: 123 diff --git a/backend/api/tests/unit/create-profile.unit.test.ts b/backend/api/tests/unit/create-profile.unit.test.ts index 77dd3d4..580b073 100644 --- a/backend/api/tests/unit/create-profile.unit.test.ts +++ b/backend/api/tests/unit/create-profile.unit.test.ts @@ -6,10 +6,7 @@ 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()} -}); +jest.mock('common/util/time'); import { createProfile } from "api/create-profile"; import * as supabaseInit from "shared/supabase/init"; @@ -24,25 +21,22 @@ import { AuthedUser } from "api/helpers/endpoint"; describe('createProfile', () => { let mockPg = {} as any; - beforeEach(() => { jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn().mockReturnValue(null), + oneOrNone: jest.fn(), one: jest.fn() }; (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); }); - afterEach(() => { jest.restoreAllMocks(); }); - describe('should', () => { - it('successfully create a profile', async () => { + describe('when given valid input', () => { + it('should successfully create a profile', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -62,29 +56,41 @@ describe('createProfile', () => { city: "mockCity" }; const mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago + createdTime: Date.now(), name: "mockName", username: "mockUserName" }; - (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: false, 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(tryCatch).toBeCalledTimes(2); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select id from profiles where user_id = $1'), + [mockAuth.uid] + ); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1); expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody); expect(sharedUtils.getUser).toBeCalledTimes(1); expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(supabaseUsers.updateUser).toBeCalledTimes(1); + expect(supabaseUsers.updateUser).toBeCalledWith( + expect.any(Object), + mockAuth.uid, + {avatarUrl: mockBody.pinned_url} + ); + expect(supabaseUtils.insert).toBeCalledTimes(1); + expect(supabaseUtils.insert).toBeCalledWith( + expect.any(Object), + 'profiles', + expect.objectContaining({user_id: 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(); @@ -102,7 +108,7 @@ describe('createProfile', () => { ); }); - it('successfully create milestone profile', async () => { + it('should successfully create milestone profile', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -127,40 +133,25 @@ describe('createProfile', () => { username: "mockUserName" }; - (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: false, 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); - - (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(mockPg.one).toBeCalledTimes(1); + expect(mockPg.one).toBeCalledWith( + expect.stringContaining('SELECT count(*) FROM profiles'), + [], + expect.any(Function) ); expect(sendDiscordMessage).toBeCalledTimes(2); - expect(sendDiscordMessage).toHaveBeenNthCalledWith( - 1, - expect.stringContaining(mockUser.name && mockUser.username), - 'members' - ); expect(sendDiscordMessage).toHaveBeenNthCalledWith( 2, expect.stringContaining(String(mockNProfiles)), @@ -168,8 +159,9 @@ describe('createProfile', () => { ); }); - - it('throws an error if it failed to track create profile', async () => { + }); + describe('when an error occurs', () => { + it('should throw if it failed to track create profile', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -193,29 +185,25 @@ describe('createProfile', () => { username: "mockUserName" }; - (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: false, 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); + (sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Track error')); await results.continue(); - expect(errorSpy).toBeCalledWith('Failed to track create profile', null) + + expect(errorSpy).toBeCalledWith( + 'Failed to track create profile', + expect.objectContaining({name: 'Error'}) + ); }); - it('throws an error if it failed to send discord new profile', async () => { + it('should throw if it failed to send discord new profile', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -241,34 +229,25 @@ describe('createProfile', () => { (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); + (sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Sending error')); 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', + expect.objectContaining({name: 'Error'}) ); - expect(errorSpy).toBeCalledWith('Failed to send discord new profile', null); }); - it('throws an error if it failed to send discord user milestone', async () => { + it('should throw if it failed to send discord user milestone', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -295,48 +274,34 @@ describe('createProfile', () => { (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); + .mockRejectedValueOnce(new Error('Discord error')); (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); + expect(errorSpy).toBeCalledWith( + 'Failed to send discord user milestone', + expect.objectContaining({name: 'Error'}) + ); }); - it('throws an error if the profile already exists', async () => { + it('should throw if the user already exists', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -350,16 +315,15 @@ describe('createProfile', () => { }; const mockAuth = {uid: '321'} as AuthedUser; const mockReq = {} as any; - const mockExistingUser = {id: "mockExistingUserId"}; - (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockExistingUser, error: null}); + (tryCatch as jest.Mock).mockResolvedValueOnce({data: true, error: null}); await expect(createProfile(mockBody, mockAuth, mockReq)) .rejects .toThrowError('User already exists'); }); - it('throws an error if the user already exists', async () => { + it('should throw if unable to find the account', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -375,16 +339,14 @@ describe('createProfile', () => { const mockReq = {} as any; (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); - (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); await expect(createProfile(mockBody, mockAuth, mockReq)) .rejects .toThrowError('Your account was not found'); - expect(sharedUtils.getUser).toBeCalledTimes(1); - expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); }); - it('throw an error if anything unexpected happens when creating the user', async () => { + it('should throw if anything unexpected happens when creating the user', async () => { const mockBody = { city: "mockCity", gender: "mockGender", @@ -406,15 +368,11 @@ describe('createProfile', () => { (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: null, error: Error}); await expect(createProfile(mockBody, mockAuth, mockReq)) .rejects .toThrowError('Error creating user'); - expect(sharedUtils.getUser).toBeCalledTimes(1); - expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/create-user.unit.test.ts b/backend/api/tests/unit/create-user.unit.test.ts index 469f99a..303ab80 100644 --- a/backend/api/tests/unit/create-user.unit.test.ts +++ b/backend/api/tests/unit/create-user.unit.test.ts @@ -507,7 +507,7 @@ describe('createUser', () => { }); describe('when an error occurs', () => { - it('should throw an error if the user already exists', async () => { + it('should throw if the user already exists', async () => { const mockProps = { deviceToken: "mockDeviceToken", adminToken: "mockAdminToken" @@ -556,7 +556,7 @@ describe('createUser', () => { .toThrowError('User already exists'); }); - it('should throw an error if the username is already taken', async () => { + it('should throw if the username is already taken', async () => { const mockProps = { deviceToken: "mockDeviceToken", adminToken: "mockAdminToken" @@ -606,7 +606,7 @@ describe('createUser', () => { .toThrowError('Username already taken'); }); - it('should throw an error if failed to track create profile', async () => { + it('should throw if failed to track create profile', async () => { const mockProps = { deviceToken: "mockDeviceToken", adminToken: "mockAdminToken" @@ -679,7 +679,7 @@ describe('createUser', () => { expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error)); }); - it('should throw an error if failed to send a welcome email', async () => { + it('should throw if failed to send a welcome email', async () => { Object.defineProperty(hostingConstants, 'IS_LOCAL', { value: false, writable: true @@ -757,7 +757,7 @@ describe('createUser', () => { expect(errorSpy).toBeCalledWith('Failed to sendWelcomeEmail', expect.any(Error)); }); - it('should throw an error if failed to set last time online', async () => { + it('should throw if failed to set last time online', async () => { const mockProps = { deviceToken: "mockDeviceToken", adminToken: "mockAdminToken" diff --git a/backend/api/tests/unit/create-vote.unit.test.ts b/backend/api/tests/unit/create-vote.unit.test.ts index 03e42e7..3b87da8 100644 --- a/backend/api/tests/unit/create-vote.unit.test.ts +++ b/backend/api/tests/unit/create-vote.unit.test.ts @@ -22,7 +22,7 @@ describe('createVote', () => { }); describe('when given valid input', () => { - it('successfully creates a vote', async () => { + it('should successfully creates a vote', async () => { const mockProps = { title: 'mockTitle', description: {'mockDescription': 'mockDescriptionValue'}, @@ -61,7 +61,7 @@ describe('createVote', () => { }); }); describe('when an error occurs', () => { - it('should throw an error if the account was not found', async () => { + it('should throw if the account was not found', async () => { const mockProps = { title: 'mockTitle', description: {'mockDescription': 'mockDescriptionValue'}, @@ -77,7 +77,7 @@ describe('createVote', () => { .toThrow('Your account was not found'); }); - it('should throw an error if unable to create a question', async () => { + it('should throw if unable to create a question', async () => { const mockProps = { title: 'mockTitle', description: {'mockDescription': 'mockDescriptionValue'}, diff --git a/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts b/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts index 3f9beed..0d6ba27 100644 --- a/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts +++ b/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts @@ -20,7 +20,7 @@ describe('deleteBookmarkedSearch', () => { }); describe('when given valid input', () => { - it('successfully deletes a bookmarked search', async () => { + it('should successfully deletes a bookmarked search', async () => { const mockProps = { id: 123 }; @@ -28,6 +28,7 @@ describe('deleteBookmarkedSearch', () => { const mockReq = {} as any; const result = await deleteBookmarkedSearch(mockProps, mockAuth, mockReq); + expect(result).toStrictEqual({}); expect(mockPg.none).toBeCalledTimes(1); expect(mockPg.none).toBeCalledWith( diff --git a/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts b/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts index 15f7115..e1191c8 100644 --- a/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts +++ b/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts @@ -54,7 +54,7 @@ describe('deleteCompatibilityAnswers', () => { }); }); describe('when an error occurs', () => { - it('should throw an error if the user is not the answers author', async () => { + it('should throw if the user is not the answers author', async () => { const mockProps = { id: 123 }; diff --git a/backend/api/tests/unit/delete-me.unit.test.ts b/backend/api/tests/unit/delete-me.unit.test.ts index 1289216..b1d6808 100644 --- a/backend/api/tests/unit/delete-me.unit.test.ts +++ b/backend/api/tests/unit/delete-me.unit.test.ts @@ -22,10 +22,10 @@ describe('deleteMe', () => { (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 = { @@ -72,13 +72,11 @@ describe('deleteMe', () => { 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 () => { @@ -88,14 +86,11 @@ describe('deleteMe', () => { 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 () => { diff --git a/backend/api/tests/unit/delete-message.unit.test.ts b/backend/api/tests/unit/delete-message.unit.test.ts index e4ed96b..8e6bd96 100644 --- a/backend/api/tests/unit/delete-message.unit.test.ts +++ b/backend/api/tests/unit/delete-message.unit.test.ts @@ -21,6 +21,7 @@ describe('deleteMessage', () => { afterEach(() => { jest.restoreAllMocks(); }); + describe('when given valid input', () => { it('should delete a message', async () => { const mockMessageId = { diff --git a/backend/api/tests/unit/edit-message.unit.test.ts b/backend/api/tests/unit/edit-message.unit.test.ts index 402777b..4339fe9 100644 --- a/backend/api/tests/unit/edit-message.unit.test.ts +++ b/backend/api/tests/unit/edit-message.unit.test.ts @@ -72,6 +72,7 @@ describe('editMessage', () => { ); }); }); + describe('when an error occurs', () => { it('should throw if there is an issue with the message', async () => { const mockProps = { diff --git a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts index 7054340..1089e50 100644 --- a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts +++ b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts @@ -16,6 +16,7 @@ describe('getCompatibilityQuestions', () => { afterEach(() => { jest.restoreAllMocks(); }); + describe('when given valid input', () => { it('should get compatibility questions', async () => { const mockProps = {} as any; 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 index eefd625..285294d 100644 --- a/backend/api/tests/unit/get-current-private-users.unit.test.ts +++ b/backend/api/tests/unit/get-current-private-users.unit.test.ts @@ -41,6 +41,7 @@ describe('getCurrentPrivateUser', () => { ); }); }); + describe('when an error occurs', () => { it('should throw if unable to get users private data', async () => { const mockAuth = { uid: '321' } as AuthedUser; diff --git a/backend/api/tests/unit/get-options.unit.test.ts b/backend/api/tests/unit/get-options.unit.test.ts index 52c262f..5ddd7a6 100644 --- a/backend/api/tests/unit/get-options.unit.test.ts +++ b/backend/api/tests/unit/get-options.unit.test.ts @@ -43,6 +43,7 @@ describe('getOptions', () => { expect(tryCatch).toBeCalledTimes(1); }); }); + describe('when an error occurs', () => { it('should throw if the table is invalid', async () => { const mockTable = "causes"; @@ -60,9 +61,6 @@ describe('getOptions', () => { 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); diff --git a/backend/api/tests/unit/get-private-messages.unit.test.ts b/backend/api/tests/unit/get-private-messages.unit.test.ts index ff6c3e5..82f1cbc 100644 --- a/backend/api/tests/unit/get-private-messages.unit.test.ts +++ b/backend/api/tests/unit/get-private-messages.unit.test.ts @@ -186,6 +186,7 @@ describe('getChannelMessagesEndpoint', () => { }); }); + describe('when an error occurs', () => { it('should throw if unable to get messages', async () => { const mockProps = { diff --git a/backend/api/tests/unit/get-profiles.unit.test.ts b/backend/api/tests/unit/get-profiles.unit.test.ts index 364eca8..2e86306 100644 --- a/backend/api/tests/unit/get-profiles.unit.test.ts +++ b/backend/api/tests/unit/get-profiles.unit.test.ts @@ -1,6 +1,7 @@ import * as profilesModule from "api/get-profiles"; import { Profile } from "common/profiles/profile"; import * as supabaseInit from "shared/supabase/init"; +import * as sqlBuilder from "shared/supabase/sql-builder"; describe('getProfiles', () => { beforeEach(() => { @@ -11,8 +12,8 @@ describe('getProfiles', () => { jest.restoreAllMocks(); }); - describe('should fetch the user profiles', () => { - it('successfully', async ()=> { + describe('when given valid input', () => { + it('should successfully return profile information and count', async ()=> { const mockProfiles = [ { diet: ['Jonathon Hammon'], @@ -27,19 +28,15 @@ describe('getProfiles', () => { has_kids: 2, } ] as Profile []; - - jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3}); - const props = { limit: 2, orderBy: "last_online_time" as const, }; const mockReq = {} as any; - const results = await profilesModule.getProfiles(props, mockReq, mockReq); - if('continue' in results) { - throw new Error('Expected direct response') - }; + jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3}); + + const results: any = await profilesModule.getProfiles(props, mockReq, mockReq); expect(results.status).toEqual('success'); expect(results.profiles).toEqual(mockProfiles); @@ -47,8 +44,10 @@ describe('getProfiles', () => { expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props); expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1); }); + }); - it('unsuccessfully', async () => { + describe('when an error occurs', () => { + it('should not return profile information', async () => { jest.spyOn(profilesModule, 'loadProfiles').mockRejectedValue(null); const props = { @@ -56,278 +55,274 @@ describe('getProfiles', () => { orderBy: "last_online_time" as const, }; const mockReq = {} as any; - const results = await profilesModule.getProfiles(props, mockReq, mockReq); - - if('continue' in results) { - throw new Error('Expected direct response') - }; + const results: any = await profilesModule.getProfiles(props, mockReq, mockReq); expect(results.status).toEqual('fail'); expect(results.profiles).toEqual([]); expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props); expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1); }); - }); }); describe('loadProfiles', () => { let mockPg: any; - - describe('should call pg.map with an SQL query', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockPg = { - map: jest.fn().mockResolvedValue([]), - one: jest.fn().mockResolvedValue(1), - }; - - jest.spyOn(supabaseInit, 'createSupabaseDirectClient') - .mockReturnValue(mockPg); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); + beforeEach(() => { + jest.clearAllMocks(); + mockPg = { + map: jest.fn(), + one: jest.fn() + }; + + jest.spyOn(supabaseInit, 'createSupabaseDirectClient') + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); - it('successfully', async () => { - await profilesModule.loadProfiles({ - limit: 10, - name: 'John', - is_smoker: true, + describe('when given valid input', () => { + describe('should call pg.map with an SQL query', () => { + it('successfully', async () => { + const mockProps = { + limit: 10, + name: 'John', + is_smoker: true, + }; + + (mockPg.map as jest.Mock).mockResolvedValue([]); + (mockPg.one as jest.Mock).mockResolvedValue(1); + jest.spyOn(sqlBuilder, 'renderSql'); + jest.spyOn(sqlBuilder, 'select'); + jest.spyOn(sqlBuilder, 'from'); + jest.spyOn(sqlBuilder, 'where'); + jest.spyOn(sqlBuilder, 'join'); + + await profilesModule.loadProfiles(mockProps); + + const [query, values, cb] = mockPg.map.mock.calls[0]; + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain('select'); + expect(query).toContain('from profiles'); + expect(query).toContain('where'); + expect(query).toContain('limit 10'); + expect(query).toContain(`John`); + expect(query).toContain(`is_smoker`); + expect(query).not.toContain(`gender`); + expect(query).not.toContain(`education_level`); + expect(query).not.toContain(`pref_gender`); + expect(query).not.toContain(`age`); + expect(query).not.toContain(`drinks_per_month`); + expect(query).not.toContain(`pref_relation_styles`); + expect(query).not.toContain(`pref_romantic_styles`); + expect(query).not.toContain(`diet`); + expect(query).not.toContain(`political_beliefs`); + expect(query).not.toContain(`religion`); + expect(query).not.toContain(`has_kids`); + expect(sqlBuilder.renderSql).toBeCalledTimes(3); + expect(sqlBuilder.select).toBeCalledTimes(3); + expect(sqlBuilder.from).toBeCalledTimes(2); + expect(sqlBuilder.where).toBeCalledTimes(8); + expect(sqlBuilder.join).toBeCalledTimes(1); }); - - const [query, values, cb] = mockPg.map.mock.calls[0] - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain('select'); - expect(query).toContain('from profiles'); - expect(query).toContain('where'); - expect(query).toContain('limit 10'); - expect(query).toContain(`John`); - expect(query).toContain(`is_smoker`); - expect(query).not.toContain(`gender`); - expect(query).not.toContain(`education_level`); - expect(query).not.toContain(`pref_gender`); - expect(query).not.toContain(`age`); - expect(query).not.toContain(`drinks_per_month`); - expect(query).not.toContain(`pref_relation_styles`); - expect(query).not.toContain(`pref_romantic_styles`); - expect(query).not.toContain(`diet`); - expect(query).not.toContain(`political_beliefs`); - expect(query).not.toContain(`religion`); - expect(query).not.toContain(`has_kids`); - }); - - it('that contains a gender filter', async () => { - await profilesModule.loadProfiles({ - genders: ['Electrical_gender'], + it('that contains a gender filter', async () => { + await profilesModule.loadProfiles({ + genders: ['Electrical_gender'], + }); + + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`gender`); + expect(query).toContain(`Electrical_gender`); }); - - const [query, values, cb] = mockPg.map.mock.calls[0] - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`gender`); - expect(query).toContain(`Electrical_gender`); - }); - - it('that contains a education level filter', async () => { - await profilesModule.loadProfiles({ - education_levels: ['High School'], + it('that contains a education level filter', async () => { + await profilesModule.loadProfiles({ + education_levels: ['High School'], + }); + + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`education_level`); + expect(query).toContain(`High School`); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`education_level`); - expect(query).toContain(`High School`); - }); - - it('that contains a prefer gender filter', async () => { - await profilesModule.loadProfiles({ - pref_gender: ['female'], + it('that contains a prefer gender filter', async () => { + await profilesModule.loadProfiles({ + pref_gender: ['female'], + }); + + const [query, values, cb] = mockPg.map.mock.calls[0] + console.log(query); + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`pref_gender`); + expect(query).toContain(`female`); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - console.log(query); - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`pref_gender`); - expect(query).toContain(`female`); - }); + it('that contains a minimum age filter', async () => { + await profilesModule.loadProfiles({ + pref_age_min: 20, + }); - it('that contains a minimum age filter', async () => { - await profilesModule.loadProfiles({ - pref_age_min: 20, + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`age`); + expect(query).toContain(`>= 20`); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`age`); - expect(query).toContain(`>= 20`); - }); + it('that contains a maximum age filter', async () => { + await profilesModule.loadProfiles({ + pref_age_max: 40, + }); - it('that contains a maximum age filter', async () => { - await profilesModule.loadProfiles({ - pref_age_max: 40, + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`age`); + expect(query).toContain(`<= 40`); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`age`); - expect(query).toContain(`<= 40`); - }); + it('that contains a minimum drinks per month filter', async () => { + await profilesModule.loadProfiles({ + drinks_min: 4, + }); - it('that contains a minimum drinks per month filter', async () => { - await profilesModule.loadProfiles({ - drinks_min: 4, + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`drinks_per_month`); + expect(query).toContain('4'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`drinks_per_month`); - expect(query).toContain('4'); - }); + it('that contains a maximum drinks per month filter', async () => { + await profilesModule.loadProfiles({ + drinks_max: 20, + }); - it('that contains a maximum drinks per month filter', async () => { - await profilesModule.loadProfiles({ - drinks_max: 20, + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`drinks_per_month`); + expect(query).toContain('20'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`drinks_per_month`); - expect(query).toContain('20'); - }); + it('that contains a relationship style filter', async () => { + await profilesModule.loadProfiles({ + pref_relation_styles: ['Chill and relaxing'], + }); - it('that contains a relationship style filter', async () => { - await profilesModule.loadProfiles({ - pref_relation_styles: ['Chill and relaxing'], + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`pref_relation_styles`); + expect(query).toContain('Chill and relaxing'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`pref_relation_styles`); - expect(query).toContain('Chill and relaxing'); - }); + it('that contains a romantic style filter', async () => { + await profilesModule.loadProfiles({ + pref_romantic_styles: ['Sexy'], + }); - it('that contains a romantic style filter', async () => { - await profilesModule.loadProfiles({ - pref_romantic_styles: ['Sexy'], + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`pref_romantic_styles`); + expect(query).toContain('Sexy'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`pref_romantic_styles`); - expect(query).toContain('Sexy'); - }); + it('that contains a diet filter', async () => { + await profilesModule.loadProfiles({ + diet: ['Glutton'], + }); - it('that contains a diet filter', async () => { - await profilesModule.loadProfiles({ - diet: ['Glutton'], + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`diet`); + expect(query).toContain('Glutton'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`diet`); - expect(query).toContain('Glutton'); - }); + it('that contains a political beliefs filter', async () => { + await profilesModule.loadProfiles({ + political_beliefs: ['For the people'], + }); - it('that contains a political beliefs filter', async () => { - await profilesModule.loadProfiles({ - political_beliefs: ['For the people'], + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`political_beliefs`); + expect(query).toContain('For the people'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`political_beliefs`); - expect(query).toContain('For the people'); - }); + it('that contains a religion filter', async () => { + await profilesModule.loadProfiles({ + religion: ['The blood god'], + }); - it('that contains a religion filter', async () => { - await profilesModule.loadProfiles({ - religion: ['The blood god'], + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`religion`); + expect(query).toContain('The blood god'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`religion`); - expect(query).toContain('The blood god'); - }); + it('that contains a has kids filter', async () => { + await profilesModule.loadProfiles({ + has_kids: 3, + }); - it('that contains a has kids filter', async () => { - await profilesModule.loadProfiles({ - has_kids: 3, + const [query, values, cb] = mockPg.map.mock.calls[0] + + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`has_kids`); + expect(query).toContain('> 0'); }); - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`has_kids`); - expect(query).toContain('> 0'); + it('should return profiles from the database', async () => { + const mockProfiles = [ + { + diet: ['Jonathon Hammon'], + is_smoker: true, + has_kids: 0 + }, + { + diet: ['Joseph Hammon'], + is_smoker: false, + has_kids: 1 + }, + { + diet: ['Jolene Hammon'], + is_smoker: true, + has_kids: 2, + } + ] as Profile []; + const props = {} as any; + + (mockPg.map as jest.Mock).mockResolvedValue(mockProfiles); + (mockPg.one as jest.Mock).mockResolvedValue(1); + + const results = await profilesModule.loadProfiles(props); + + expect(results).toEqual({profiles: mockProfiles, count: 1}); + }); }); }); - describe('should', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockPg = { - map: jest.fn(), - one: jest.fn().mockResolvedValue(1), - }; - - jest.spyOn(supabaseInit, 'createSupabaseDirectClient') - .mockReturnValue(mockPg) - - - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('return profiles from the database', async () => { - const mockProfiles = [ - { - diet: ['Jonathon Hammon'], - is_smoker: true, - has_kids: 0 - }, - { - diet: ['Joseph Hammon'], - is_smoker: false, - has_kids: 1 - }, - { - diet: ['Jolene Hammon'], - is_smoker: true, - has_kids: 2, - } - ] as Profile []; - - mockPg.map.mockResolvedValue(mockProfiles); - const props = {} as any; - const results = await profilesModule.loadProfiles(props); - - expect(results).toEqual({profiles: mockProfiles, count: 1}); - }); - - it('throw an error if there is no compatability', async () => { + describe('when an error occurs', () => { + it('throw if there is no compatability', async () => { const props = { orderBy: 'compatibility_score' } + expect(profilesModule.loadProfiles(props)) .rejects .toThrowError('Incompatible with user ID') }); - }) -}) \ No newline at end of file + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/get-users.unit.test.ts b/backend/api/tests/unit/get-users.unit.test.ts index 27a15e7..929ad6e 100644 --- a/backend/api/tests/unit/get-users.unit.test.ts +++ b/backend/api/tests/unit/get-users.unit.test.ts @@ -1,163 +1,90 @@ jest.mock("shared/supabase/init"); +jest.mock("common/supabase/users"); +jest.mock("common/api/user-types"); import { getUser } from "api/get-user"; -import { createSupabaseDirectClient } from "shared/supabase/init"; +import * as supabaseInit from "shared/supabase/init"; import { toUserAPIResponse } from "common/api/user-types"; -import { convertUser } from "common/supabase/users"; -import { APIError } from "common/api/utils"; - - -jest.spyOn(require("common/supabase/users"), 'convertUser') -jest.spyOn(require("common/api/user-types"), 'toUserAPIResponse') describe('getUser', () =>{ let mockPg: any; beforeEach(() => { + jest.resetAllMocks(); mockPg = { oneOrNone: jest.fn(), }; - (createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg); - jest.clearAllMocks(); + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); }); - describe('when fetching by id', () => { - it('should fetch user successfully by id', async () => { - const mockDbUser = { - created_time: '2025-11-11T16:42:05.188Z', - data: { link: {}, avatarUrl: "", isBannedFromPosting: false }, - id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP', - name: 'Franklin Buckridge', - name_username_vector: "'buckridg':2,4 'franklin':1,3", - username: 'Franky_Buck' - }; - const mockConvertedUser = { - created_time: new Date(), - id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP', - name: 'Franklin Buckridge', - name_username_vector: "'buckridg':2,4 'franklin':1,3", - username: 'Franky_Buck' - - }; - const mockApiResponse = { - created_time: '2025-11-11T16:42:05.188Z', - data: { link: {}, avatarUrl: "", isBannedFromPosting: false }, - id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP', - name: 'Franklin Buckridge', - username: 'Franky_Buck' - }; - - mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => { - const result = cb(mockDbUser); - return Promise.resolve(result); + describe('when given valid input', () => { + describe('and fetching by id', () => { + it('should fetch user successfully by id', async () => { + const mockProps = {id: "mockId"}; + const mockUser = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser); + (toUserAPIResponse as jest.Mock).mockReturnValue('mockApiResponse'); + + const result = await getUser(mockProps); + + expect(result).toBe('mockApiResponse'); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select * from users'), + [mockProps.id], + expect.any(Function) + ); + expect(toUserAPIResponse).toBeCalledTimes(1); + expect(toUserAPIResponse).toBeCalledWith(mockUser); }); - - (convertUser as jest.Mock).mockReturnValue(mockConvertedUser); - ( toUserAPIResponse as jest.Mock).mockReturnValue(mockApiResponse); - - const result = await getUser({id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP'}) - - expect(mockPg.oneOrNone).toHaveBeenCalledWith( - expect.stringContaining('where id = $1'), - ['feUaIfcxVmJZHJOVVfawLTTPgZiP'], - expect.any(Function) - ); - - expect(convertUser).toHaveBeenCalledWith(mockDbUser); - expect(toUserAPIResponse).toHaveBeenCalledWith(mockConvertedUser); - - expect(result).toEqual(mockApiResponse); - }); - it('should throw 404 when user is not found by id', async () => { - mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => { - return Promise.resolve(null); - }); + describe('when fetching by username', () => { + it('should fetch user successfully by username', async () => { + const mockProps = {username: "mockUsername"}; + const mockUser = {} as any; - (convertUser as jest.Mock).mockReturnValue(null) - - try { - await getUser({id: '3333'}); - fail('Should have thrown'); - } catch (error) { - const apiError = error as APIError; - expect(apiError.code).toBe(404) - expect(apiError.message).toBe('User not found') - expect(apiError.details).toBeUndefined() - expect(apiError.name).toBe('APIError') - } - }) + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser); - }) - - describe('when fetching by username', () => { - it('should fetch user successfully by username', async () => { - const mockDbUser = { - created_time: '2025-11-11T16:42:05.188Z', - data: { link: {}, avatarUrl: "", isBannedFromPosting: false }, - id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP', - name: 'Franklin Buckridge', - name_username_vector: "'buckridg':2,4 'franklin':1,3", - username: 'Franky_Buck' - }; - const mockConvertedUser = { - created_time: new Date(), - id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP', - name: 'Franklin Buckridge', - name_username_vector: "'buckridg':2,4 'franklin':1,3", - username: 'Franky_Buck' - - }; - const mockApiResponse = { - created_time: '2025-11-11T16:42:05.188Z', - data: { link: {}, avatarUrl: "", isBannedFromPosting: false }, - id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP', - name: 'Franklin Buckridge', - username: 'Franky_Buck' - }; - - mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => { - const result = cb(mockDbUser); - return Promise.resolve(result); + await getUser(mockProps) + + expect(mockPg.oneOrNone).toHaveBeenCalledWith( + expect.stringContaining('where username = $1'), + [mockProps.username], + expect.any(Function) + ); }); - - (convertUser as jest.Mock).mockReturnValue(mockConvertedUser); - (toUserAPIResponse as jest.Mock).mockReturnValue(mockApiResponse); - - const result = await getUser({username: 'Franky_Buck'}) - - expect(mockPg.oneOrNone).toHaveBeenCalledWith( - expect.stringContaining('where username = $1'), - ['Franky_Buck'], - expect.any(Function) - ); - - expect(convertUser).toHaveBeenCalledWith(mockDbUser); - expect(toUserAPIResponse).toHaveBeenCalledWith(mockConvertedUser); - - expect(result).toEqual(mockApiResponse); - }); + }); + + describe('when an error occurs', () => { + describe('and fetching by id', () => { + it('should throw when user is not found by id', async () => { + const mockProps = {id: "mockId"}; - it('should throw 404 when user is not found by id', async () => { - mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => { - return Promise.resolve(null); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + expect(getUser(mockProps)) + .rejects + .toThrow('User not found'); }); + }); + describe('when fetching by username', () => { + it('should throw when user is not found by id', async () => { + const mockProps = {username: "mockUsername"}; - (convertUser as jest.Mock).mockReturnValue(null) - - try { - await getUser({username: '3333'}); - fail('Should have thrown'); - } catch (error) { - const apiError = error as APIError; - expect(apiError.code).toBe(404) - expect(apiError.message).toBe('User not found') - expect(apiError.details).toBeUndefined() - expect(apiError.name).toBe('APIError') - } - }) - }) -}) \ No newline at end of file + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + expect(getUser(mockProps)) + .rejects + .toThrow('User not found'); + }); + }); + }); +}); \ 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 index b9f02cc..83ae3e7 100644 --- a/backend/api/tests/unit/hide-comment.unit.test.ts +++ b/backend/api/tests/unit/hide-comment.unit.test.ts @@ -45,12 +45,11 @@ describe('hideComment', () => { user_name: "mockUserName", user_username: "mockUserUsername", }; + const mockConvertedComment = "mockConvertedCommentValue"; (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); + (convertComment as jest.Mock).mockReturnValue(mockConvertedComment); await hideComment(mockProps, mockAuth, mockReq); @@ -64,7 +63,7 @@ describe('hideComment', () => { expect(convertComment).toBeCalledTimes(1); expect(convertComment).toBeCalledWith(mockComment); expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1); - expect(websocketHelpers.broadcastUpdatedComment).toBeCalledWith(null); + expect(websocketHelpers.broadcastUpdatedComment).toBeCalledWith(mockConvertedComment); }); it('should successfully hide the comment if the user is the one who made the comment', async () => { @@ -89,9 +88,6 @@ describe('hideComment', () => { (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); }); @@ -118,9 +114,6 @@ describe('hideComment', () => { (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); }); @@ -134,7 +127,7 @@ describe('hideComment', () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); expect(hideComment(mockProps, mockAuth, mockReq)) .rejects @@ -163,9 +156,6 @@ describe('hideComment', () => { (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 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 index 8c8e09b..82f85f4 100644 --- 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 @@ -29,12 +29,11 @@ describe('leavePrivateUserMessageChannel', () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; const mockUser = { name: "mockName" }; + const mockLeaveChatContent = "mockLeaveChatContentValue"; (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); + (messageHelpers.leaveChatContent as jest.Mock).mockReturnValue(mockLeaveChatContent); const results = await leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq); @@ -56,7 +55,7 @@ describe('leavePrivateUserMessageChannel', () => { expect(messageHelpers.leaveChatContent).toBeCalledWith(mockUser.name); expect(messageHelpers.insertPrivateMessage).toBeCalledTimes(1); expect(messageHelpers.insertPrivateMessage).toBeCalledWith( - null, + mockLeaveChatContent, mockProps.channelId, mockAuth.uid, 'system_status', @@ -70,7 +69,7 @@ describe('leavePrivateUserMessageChannel', () => { const mockAuth = { uid: '321' } as AuthedUser; const mockReq = {} as any; - (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq)) .rejects diff --git a/backend/api/tests/unit/like-profile.unit.test.ts b/backend/api/tests/unit/like-profile.unit.test.ts index 858f910..80e5738 100644 --- a/backend/api/tests/unit/like-profile.unit.test.ts +++ b/backend/api/tests/unit/like-profile.unit.test.ts @@ -41,12 +41,10 @@ describe('likeProfile', () => { 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); @@ -79,7 +77,6 @@ describe('likeProfile', () => { 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); @@ -101,7 +98,6 @@ describe('likeProfile', () => { 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); @@ -130,7 +126,6 @@ describe('likeProfile', () => { target_id: "mockTargetId" }; - (mockPg.none as jest.Mock).mockResolvedValue(null); (tryCatch as jest.Mock) .mockResolvedValueOnce({data: mockData, error: Error}); @@ -153,7 +148,6 @@ describe('likeProfile', () => { target_id: "mockTargetId" }; - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); (tryCatch as jest.Mock) .mockResolvedValueOnce({data: false}) .mockResolvedValueOnce({data: mockData, error: null}); @@ -179,7 +173,6 @@ describe('likeProfile', () => { target_id: "mockTargetId" }; - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); (tryCatch as jest.Mock) .mockResolvedValueOnce({data: false}) .mockResolvedValueOnce({data: mockData, error: Error}); 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 index 0f4e41a..8d20039 100644 --- a/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts +++ b/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts @@ -25,8 +25,6 @@ describe('markAllNotifsRead', () => { 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); diff --git a/backend/api/tests/unit/react-to-message.unit.test.ts b/backend/api/tests/unit/react-to-message.unit.test.ts new file mode 100644 index 0000000..b4d213b --- /dev/null +++ b/backend/api/tests/unit/react-to-message.unit.test.ts @@ -0,0 +1,139 @@ +jest.mock('shared/supabase/init'); +jest.mock('api/helpers/private-messages'); + +import { reactToMessage } from "api/react-to-message"; +import * as supabaseInit from "shared/supabase/init"; +import * as messageHelpers from "api/helpers/private-messages"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('reactToMessage', () => { + 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 return success', async () => { + const mockProps = { + messageId: 123, + reaction: "mockReaction", + toDelete: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockMessage = { channel_id: "mockChannelId"}; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null); + + const result = await reactToMessage(mockProps, mockAuth, mockReq); + const [sql, params] = mockPg.oneOrNone.mock.calls[0] + const [sql1, params1] = mockPg.none.mock.calls[0] + + expect(result.success).toBeTruthy(); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(params).toEqual([mockAuth.uid, mockProps.messageId]) + expect(sql).toEqual( + expect.stringContaining('SELECT *') + ); + expect(sql).toEqual( + expect.stringContaining('FROM private_user_message_channel_members m') + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(params1).toEqual([mockProps.reaction, mockAuth.uid, mockProps.messageId]) + expect(sql1).toEqual( + expect.stringContaining('UPDATE private_user_messages') + ); + expect(sql1).toEqual( + expect.stringContaining('SET reactions =') + ); + expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1); + expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith( + expect.any(Object), + mockMessage.channel_id, + mockAuth.uid + ); + }); + + it('should return success when removing a reaction', async () => { + const mockProps = { + messageId: 123, + reaction: "mockReaction", + toDelete: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockMessage = { channel_id: "mockChannelId"}; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null); + + const result = await reactToMessage(mockProps, mockAuth, mockReq); + const [sql, params] = mockPg.oneOrNone.mock.calls[0] + const [sql1, params1] = mockPg.none.mock.calls[0] + + expect(result.success).toBeTruthy(); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledTimes(1); + expect(params1).toEqual([mockProps.reaction, mockProps.messageId, mockAuth.uid]) + expect(sql1).toEqual( + expect.stringContaining('UPDATE private_user_messages') + ); + expect(sql1).toEqual( + expect.stringContaining('SET reactions = reactions - $1') + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if user does not have the authorization to react', async () => { + const mockProps = { + messageId: 123, + reaction: "mockReaction", + toDelete: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + expect(reactToMessage(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Not authorized to react to this message'); + }); + + it('should return success', async () => { + const mockProps = { + messageId: 123, + reaction: "mockReaction", + toDelete: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockMessage = { channel_id: "mockChannelId"}; + + (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 reactToMessage(mockProps, mockAuth, mockReq); + + expect(errorSpy).toBeCalledWith( + expect.stringContaining('broadcastPrivateMessages failed'), + expect.any(Error) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/remove-pinned-photo.unit.test.ts b/backend/api/tests/unit/remove-pinned-photo.unit.test.ts new file mode 100644 index 0000000..d77c73b --- /dev/null +++ b/backend/api/tests/unit/remove-pinned-photo.unit.test.ts @@ -0,0 +1,75 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/envs/constants'); +jest.mock('common/util/try-catch'); + +import { removePinnedPhoto } from "api/remove-pinned-photo"; +import * as supabaseInit from "shared/supabase/init"; +import * as envConstants from "common/envs/constants"; +import { tryCatch } from "common/util/try-catch"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('removePinnedPhoto', () => { + 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 return success', async () => { + const mockBody = { userId: "mockUserId"}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(envConstants, 'isAdminId').mockReturnValue(true); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({error: null}); + + const result: any = await removePinnedPhoto(mockBody, mockAuth, mockReq); + + expect(result.success).toBeTruthy(); + expect(envConstants.isAdminId).toBeCalledTimes(1); + expect(envConstants.isAdminId).toBeCalledWith(mockAuth.uid); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('update profiles set pinned_url = null where user_id = $1'), + [mockBody.userId] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if user auth is not an admin', async () => { + const mockBody = { userId: "mockUserId"}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(envConstants, 'isAdminId').mockReturnValue(false); + + expect(removePinnedPhoto(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Only admins can remove pinned photo'); + }); + + it('should throw if failed to remove the pinned photo', async () => { + const mockBody = { userId: "mockUserId"}; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(envConstants, 'isAdminId').mockReturnValue(true); + (mockPg.none as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({error: Error}); + + expect(removePinnedPhoto(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Failed to remove pinned photo'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/report.unit.test.ts b/backend/api/tests/unit/report.unit.test.ts new file mode 100644 index 0000000..ece72c9 --- /dev/null +++ b/backend/api/tests/unit/report.unit.test.ts @@ -0,0 +1,225 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/utils'); +jest.mock('common/discord/core'); + +import { report } from "api/report"; +import * as supabaseInit from "shared/supabase/init"; +import { tryCatch } from "common/util/try-catch"; +import * as supabaseUtils from "shared/supabase/utils"; +import { sendDiscordMessage } from "common/discord/core"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('report', () => { + 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 successfully file a report', async () => { + const mockBody = { + contentOwnerId: "mockContentOwnerId", + contentType: "user" as "user" | "comment" | "contract", + contentId: "mockContentId", + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockReporter = { + created_time: "mockCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }; + const mockReported = { + created_time: "mockCreatedTimeReported", + data: {"mockDataReported" : "mockDataValueReported"}, + id: "mockIdReported", + name: "mockNameReported", + name_username_vector: "mockNameUsernameVectorReported", + username: "mockUsernameReported", + }; + + (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + + const result = await report(mockBody, mockAuth, mockReq); + + expect(result.success).toBeTruthy(); + expect(result.result).toStrictEqual({}); + + (mockPg.oneOrNone as jest.Mock) + .mockReturnValueOnce(null) + .mockReturnValueOnce(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: mockReporter, error: null}) + .mockResolvedValueOnce({data: mockReported, error: null}); + (sendDiscordMessage as jest.Mock).mockResolvedValue(null); + + await result.continue(); + + expect(mockPg.oneOrNone).toBeCalledTimes(2); + expect(mockPg.oneOrNone).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('select * from users where id = $1'), + [mockAuth.uid] + ); + expect(mockPg.oneOrNone).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('select * from users where id = $1'), + [mockBody.contentOwnerId] + ); + expect(sendDiscordMessage).toBeCalledTimes(1); + expect(sendDiscordMessage).toBeCalledWith( + expect.stringContaining('**New Report**'), + 'reports' + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if failed to create the report', async () => { + const mockBody = { + contentOwnerId: "mockContentOwnerId", + contentType: "user" as "user" | "comment" | "contract", + contentId: "mockContentId", + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + + expect(report(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Failed to create report: '); + }); + + it('should throw if unable to get information about the user', async () => { + const mockBody = { + contentOwnerId: "mockContentOwnerId", + contentType: "user" as "user" | "comment" | "contract", + contentId: "mockContentId", + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + + const result = await report(mockBody, mockAuth, mockReq); + + (mockPg.oneOrNone as jest.Mock) + .mockReturnValueOnce(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: null, error: Error}); + + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await result.continue(); + + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to get user for report'), + expect.objectContaining({name: 'Error'}) + ); + }); + + it('should throw if unable to get information about the user being reported', async () => { + const mockBody = { + contentOwnerId: "mockContentOwnerId", + contentType: "user" as "user" | "comment" | "contract", + contentId: "mockContentId", + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockReporter = { + created_time: "mockCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }; + + (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + + const result = await report(mockBody, mockAuth, mockReq); + + (mockPg.oneOrNone as jest.Mock) + .mockReturnValueOnce(null) + .mockReturnValueOnce(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: mockReporter, error: null}) + .mockResolvedValueOnce({data: null, error: Error}); + + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await result.continue(); + + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to get reported user for report'), + expect.objectContaining({name: 'Error'}) + ); + }); + + it('should throw if failed to send discord report', async () => { + const mockBody = { + contentOwnerId: "mockContentOwnerId", + contentType: "user" as "user" | "comment" | "contract", + contentId: "mockContentId", + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockReporter = { + created_time: "mockCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }; + const mockReported = { + created_time: "mockCreatedTimeReported", + data: {"mockDataReported" : "mockDataValueReported"}, + id: "mockIdReported", + name: "mockNameReported", + name_username_vector: "mockNameUsernameVectorReported", + username: "mockUsernameReported", + }; + + (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + + const result = await report(mockBody, mockAuth, mockReq); + + (mockPg.oneOrNone as jest.Mock) + .mockReturnValueOnce(null) + .mockReturnValueOnce(null); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: mockReporter, error: null}) + .mockResolvedValueOnce({data: mockReported, error: null}); + (sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Discord error')); + + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await result.continue(); + + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to send discord reports'), + expect.any(Error) + ); + + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/save-subscription-mobile.unit.test.ts b/backend/api/tests/unit/save-subscription-mobile.unit.test.ts new file mode 100644 index 0000000..8fdb6f1 --- /dev/null +++ b/backend/api/tests/unit/save-subscription-mobile.unit.test.ts @@ -0,0 +1,70 @@ +jest.mock('shared/supabase/init'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { saveSubscriptionMobile } from "api/save-subscription-mobile"; +import * as supabaseInit from "shared/supabase/init"; + +describe('saveSubscriptionMobile', () => { + 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 return success after saving the subscription', async () => { + const mockBody = { token: "mockToken" }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + + const result = await saveSubscriptionMobile(mockBody, mockAuth, mockReq); + + expect(result.success).toBeTruthy(); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('insert into push_subscriptions_mobile(token, platform, user_id)'), + [mockBody.token, 'android', mockAuth.uid] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if token is invalid', async () => { + const mockBody = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + expect(saveSubscriptionMobile(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Invalid subscription object'); + + }); + + it('should throw if unable to save subscription', async () => { + const mockBody = { token: "mockToken" }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockRejectedValue(new Error('Saving error')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(saveSubscriptionMobile(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Failed to save subscription'); + // expect(errorSpy).toBeCalledTimes(1); + // expect(errorSpy).toBeCalledWith( + // expect.stringContaining('Error saving subscription'), + // expect.any(Error) + // ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/save-subscription.unit.test.ts b/backend/api/tests/unit/save-subscription.unit.test.ts new file mode 100644 index 0000000..2d3b31b --- /dev/null +++ b/backend/api/tests/unit/save-subscription.unit.test.ts @@ -0,0 +1,118 @@ +jest.mock('shared/supabase/init'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { saveSubscription } from "api/save-subscription"; +import * as supabaseInit from "shared/supabase/init"; + +describe('saveSubscription', () => { + 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 save user subscription', async () => { + const mockBody = { + subscription: { + endpoint: "mockEndpoint", + keys: "mockKeys", + } + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExists = { id: "mockId" }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockExists); + (mockPg.none as jest.Mock).mockResolvedValue(null); + + const result = await saveSubscription(mockBody, mockAuth, mockReq); + + expect(result.success).toBeTruthy(); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select id from push_subscriptions where endpoint = $1'), + [mockBody.subscription.endpoint] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('update push_subscriptions set keys = $1, user_id = $2 where id = $3'), + [mockBody.subscription.keys, mockAuth.uid, mockExists.id] + ); + }); + + it('should save user subscription even if this is their first one', async () => { + const mockBody = { + subscription: { + endpoint: "mockEndpoint", + keys: "mockKeys", + } + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + (mockPg.none as jest.Mock).mockResolvedValue(null); + + const result = await saveSubscription(mockBody, mockAuth, mockReq); + + expect(result.success).toBeTruthy(); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select id from push_subscriptions where endpoint = $1'), + [mockBody.subscription.endpoint] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('insert into push_subscriptions(endpoint, keys, user_id) values($1, $2, $3)'), + [mockBody.subscription.endpoint, mockBody.subscription.keys, mockAuth.uid] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if the subscription object is invalid', async () => { + const mockBody = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + expect(saveSubscription(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Invalid subscription object'); + }); + + it('should throw if unable to save subscription', async () => { + const mockBody = { + subscription: { + endpoint: "mockEndpoint", + keys: "mockKeys", + } + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExists = { id: "mockId" }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockExists); + (mockPg.none as jest.Mock).mockRejectedValue(new Error('Saving error')); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(saveSubscription(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Failed to save subscription'); + + // expect(errorSpy).toBeCalledTimes(1); + // expect(errorSpy).toBeCalledWith( + // expect.stringContaining('Error saving subscription'), + // expect.any(Error) + // ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/search-location.unit.test.ts b/backend/api/tests/unit/search-location.unit.test.ts new file mode 100644 index 0000000..f95a3f1 --- /dev/null +++ b/backend/api/tests/unit/search-location.unit.test.ts @@ -0,0 +1,36 @@ +jest.mock('common/geodb'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { searchLocation } from "api/search-location"; +import * as geodbModules from "common/geodb"; + +describe('searchLocation', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return search location', async () => { + const mockBody = { + term: "mockTerm", + limit: 15 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockReturn = "Pass"; + + (geodbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn); + + const result = await searchLocation(mockBody, mockAuth, mockReq); + + expect(result).toBe(mockReturn); + expect(geodbModules.geodbFetch).toBeCalledTimes(1); + expect(geodbModules.geodbFetch).toBeCalledWith( + expect.stringContaining(`/cities?namePrefix=${mockBody.term}&limit=${mockBody.limit}&offset=0&sort=-population`) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/search-near-city.unit.test.ts b/backend/api/tests/unit/search-near-city.unit.test.ts new file mode 100644 index 0000000..84a337e --- /dev/null +++ b/backend/api/tests/unit/search-near-city.unit.test.ts @@ -0,0 +1,72 @@ +jest.mock('common/geodb'); + +import * as citySearchModules from "api/search-near-city"; +import * as geoDbModules from "common/geodb"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('searchNearCity', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return locations near a city', async () => { + const mockBody = { + radius: 123, + cityId: "mockCityId" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockReturn = "Pass"; + + (geoDbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn); + + const result = await citySearchModules.searchNearCity(mockBody, mockAuth, mockReq); + + expect(result).toBe(mockReturn); + expect(geoDbModules.geodbFetch).toBeCalledTimes(1); + expect(geoDbModules.geodbFetch).toBeCalledWith( + expect.stringContaining(`/cities/${mockBody.cityId}/nearbyCities?radius=${mockBody.radius}&offset=0&sort=-population&limit=100`) + ); + }); + }); +}); + +describe('getNearCity', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return locations near a city', async () => { + const mockBody = { + radius: 123, + cityId: "mockCityId" + }; + const mockReturn = { + status: "mockStatus", + data: { + data: [ + { id: "mockId" } + ] + } + }; + + (geoDbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn); + + const result = await citySearchModules.getNearbyCities(mockBody.cityId, mockBody.radius); + + expect(result).toStrictEqual([mockReturn.data.data[0].id]); + expect(geoDbModules.geodbFetch).toBeCalledTimes(1); + expect(geoDbModules.geodbFetch).toBeCalledWith( + expect.stringContaining(`/cities/${mockBody.cityId}/nearbyCities?radius=${mockBody.radius}&offset=0&sort=-population&limit=100`) + ); + }); + }); +}); diff --git a/backend/api/tests/unit/search-users.unit.test.ts b/backend/api/tests/unit/search-users.unit.test.ts new file mode 100644 index 0000000..68c7127 --- /dev/null +++ b/backend/api/tests/unit/search-users.unit.test.ts @@ -0,0 +1,154 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/helpers/search'); +jest.mock('shared/supabase/sql-builder'); +jest.mock('common/supabase/users'); +jest.mock('common/api/user-types'); + +import { searchUsers } from "api/search-users"; +import * as supabaseInit from "shared/supabase/init"; +import * as searchHelpers from "shared/helpers/search"; +import * as sqlBuilderModules from "shared/supabase/sql-builder"; +import * as supabaseUsers from "common/supabase/users"; +import { toUserAPIResponse } from "common/api/user-types"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('searchUsers', () => { + 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 an array of uniq users', async () => { + const mockProps = { + term: "mockTerm", + limit: 10, + page: 1 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockSearchAllSql = "mockSQL"; + const mockAllUser = [ + {id: "mockId 1"}, + {id: "mockId 2"}, + {id: "mockId 3"}, + ]; + + (sqlBuilderModules.renderSql as jest.Mock).mockReturnValue(mockSearchAllSql); + (sqlBuilderModules.select as jest.Mock).mockReturnValue('Select'); + (sqlBuilderModules.from as jest.Mock).mockReturnValue('From'); + (sqlBuilderModules.where as jest.Mock).mockReturnValue('Where'); + (searchHelpers.constructPrefixTsQuery as jest.Mock).mockReturnValue('ConstructPrefix'); + (sqlBuilderModules.orderBy as jest.Mock).mockReturnValue('OrderBy'); + (sqlBuilderModules.limit as jest.Mock).mockReturnValue('Limit'); + (supabaseUsers.convertUser as jest.Mock).mockResolvedValue(null); + (mockPg.map as jest.Mock).mockResolvedValue(mockAllUser); + (toUserAPIResponse as jest.Mock) + .mockReturnValueOnce(mockAllUser[0].id) + .mockReturnValueOnce(mockAllUser[1].id) + .mockReturnValueOnce(mockAllUser[2].id); + + const result: any = await searchUsers(mockProps, mockAuth, mockReq); + + expect(result[0]).toContain(mockAllUser[0].id); + expect(result[1]).toContain(mockAllUser[1].id); + expect(result[2]).toContain(mockAllUser[2].id); + + expect(sqlBuilderModules.renderSql).toBeCalledTimes(1); + expect(sqlBuilderModules.renderSql).toBeCalledWith( + ['Select', 'From'], + ['Where', 'OrderBy'], + 'Limit' + ); + + expect(sqlBuilderModules.select).toBeCalledTimes(1); + expect(sqlBuilderModules.select).toBeCalledWith('*'); + expect(sqlBuilderModules.from).toBeCalledTimes(1); + expect(sqlBuilderModules.from).toBeCalledWith('users'); + expect(sqlBuilderModules.where).toBeCalledTimes(1); + expect(sqlBuilderModules.where).toBeCalledWith( + expect.stringContaining("name_username_vector @@ websearch_to_tsquery('english', $1)"), + [mockProps.term, 'ConstructPrefix'] + ); + expect(sqlBuilderModules.orderBy).toBeCalledTimes(1); + expect(sqlBuilderModules.orderBy).toBeCalledWith( + expect.stringContaining("ts_rank(name_username_vector, websearch_to_tsquery($1)) desc,"), + [mockProps.term] + ); + expect(sqlBuilderModules.limit).toBeCalledTimes(1); + expect(sqlBuilderModules.limit).toBeCalledWith(mockProps.limit, mockProps.page * mockProps.limit); + expect(mockPg.map).toBeCalledTimes(1); + expect(mockPg.map).toBeCalledWith( + mockSearchAllSql, + null, + expect.any(Function) + ); + }); + + it('should return an array of uniq users if no term is supplied', async () => { + const mockProps = { + limit: 10, + page: 1 + } as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockSearchAllSql = "mockSQL"; + const mockAllUser = [ + {id: "mockId 1"}, + {id: "mockId 2"}, + {id: "mockId 3"}, + ]; + + (sqlBuilderModules.renderSql as jest.Mock).mockReturnValue(mockSearchAllSql); + (sqlBuilderModules.select as jest.Mock).mockReturnValue('Select'); + (sqlBuilderModules.from as jest.Mock).mockReturnValue('From'); + (sqlBuilderModules.orderBy as jest.Mock).mockReturnValue('OrderBy'); + (sqlBuilderModules.limit as jest.Mock).mockReturnValue('Limit'); + (supabaseUsers.convertUser as jest.Mock).mockResolvedValue(null); + (mockPg.map as jest.Mock).mockResolvedValue(mockAllUser); + (toUserAPIResponse as jest.Mock) + .mockReturnValueOnce(mockAllUser[0].id) + .mockReturnValueOnce(mockAllUser[1].id) + .mockReturnValueOnce(mockAllUser[2].id); + + const result: any = await searchUsers(mockProps, mockAuth, mockReq); + + expect(result[0]).toContain(mockAllUser[0].id); + expect(result[1]).toContain(mockAllUser[1].id); + expect(result[2]).toContain(mockAllUser[2].id); + + expect(sqlBuilderModules.renderSql).toBeCalledTimes(1); + expect(sqlBuilderModules.renderSql).toBeCalledWith( + ['Select', 'From'], + 'OrderBy', + 'Limit' + ); + + expect(sqlBuilderModules.select).toBeCalledTimes(1); + expect(sqlBuilderModules.select).toBeCalledWith('*'); + expect(sqlBuilderModules.from).toBeCalledTimes(1); + expect(sqlBuilderModules.from).toBeCalledWith('users'); + expect(sqlBuilderModules.orderBy).toBeCalledTimes(1); + expect(sqlBuilderModules.orderBy).toBeCalledWith( + expect.stringMatching(`data->'creatorTraders'->'allTime' desc nulls last`) + ); + expect(sqlBuilderModules.limit).toBeCalledTimes(1); + expect(sqlBuilderModules.limit).toBeCalledWith(mockProps.limit, mockProps.page * mockProps.limit); + expect(mockPg.map).toBeCalledTimes(1); + expect(mockPg.map).toBeCalledWith( + mockSearchAllSql, + null, + expect.any(Function) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/send-search-notifications.unit.test.ts b/backend/api/tests/unit/send-search-notifications.unit.test.ts new file mode 100644 index 0000000..cf7d23c --- /dev/null +++ b/backend/api/tests/unit/send-search-notifications.unit.test.ts @@ -0,0 +1,314 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/sql-builder'); +jest.mock('api/get-profiles'); +jest.mock('email/functions/helpers'); +jest.mock('lodash'); + +import * as searchNotificationModules from "api/send-search-notifications"; +import * as supabaseInit from "shared/supabase/init"; +import * as sqlBuilderModules from "shared/supabase/sql-builder"; +import * as profileModules from "api/get-profiles"; +import * as helperModules from "email/functions/helpers"; +import * as lodashModules from "lodash"; + +describe('sendSearchNotification', () => { + 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 send search notification emails', async () => { + const mockSearchQuery = "mockSqlQuery"; + const mockSearches = [ + { + created_time: "mockSearchCreatedTime", + creator_id: "mockCreatorId", + id: 123, + last_notified_at: null, + location: {"mockLocation" : "mockLocationValue"}, + search_filters: null, + search_name: null, + }, + { + created_time: "mockCreatedTime1", + creator_id: "mockCreatorId1", + id: 1234, + last_notified_at: null, + location: {"mockLocation1" : "mockLocationValue1"}, + search_filters: null, + search_name: null, + }, + ]; + const _mockUsers = [ + { + created_time: "mockUserCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }, + { + created_time: "mockUserCreatedTime1", + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1", + name: "mockName1", + name_username_vector: "mockNameUsernameVector1", + username: "mockUsername1", + }, + ]; + const mockUsers = { + "user1": { + created_time: "mockUserCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }, + "user2": { + created_time: "mockUserCreatedTime1", + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1", + name: "mockName1", + name_username_vector: "mockNameUsernameVector1", + username: "mockUsername1", + }, + }; + const _mockPrivateUsers = [ + { + data: {"mockData" : "mockDataValue"}, + id: "mockId" + }, + { + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1" + }, + ]; + const mockPrivateUsers = { + "privateUser1": { + data: {"mockData" : "mockDataValue"}, + id: "mockId" + }, + "privateUser2": { + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1" + }, + }; + const mockProfiles = [ + { + name: "mockProfileName", + username: "mockProfileUsername" + }, + { + name: "mockProfileName1", + username: "mockProfileUsername1" + }, + ]; + const mockProps = [ + { + skipId: "mockCreatorId", + lastModificationWithin: '24 hours', + shortBio: true, + }, + { + skipId: "mockCreatorId1", + lastModificationWithin: '24 hours', + shortBio: true, + }, + ]; + (sqlBuilderModules.renderSql as jest.Mock) + .mockReturnValueOnce(mockSearchQuery) + .mockReturnValueOnce('usersRenderSql') + .mockReturnValueOnce('privateUsersRenderSql'); + (sqlBuilderModules.select as jest.Mock).mockReturnValue('Select'); + (sqlBuilderModules.from as jest.Mock).mockReturnValue('From'); + (mockPg.map as jest.Mock) + .mockResolvedValueOnce(mockSearches) + .mockResolvedValueOnce(_mockUsers) + .mockResolvedValueOnce(_mockPrivateUsers); + (lodashModules.keyBy as jest.Mock) + .mockReturnValueOnce(mockUsers) + .mockReturnValueOnce(mockPrivateUsers); + (profileModules.loadProfiles as jest.Mock) + .mockResolvedValueOnce({profiles: mockProfiles}) + .mockResolvedValueOnce({profiles: mockProfiles}); + jest.spyOn(searchNotificationModules, 'notifyBookmarkedSearch'); + (helperModules.sendSearchAlertsEmail as jest.Mock).mockResolvedValue(null); + + const result = await searchNotificationModules.sendSearchNotifications(); + + expect(result.status).toBe('success'); + expect(sqlBuilderModules.renderSql).toBeCalledTimes(3); + expect(sqlBuilderModules.renderSql).toHaveBeenNthCalledWith( + 1, + 'Select', + 'From' + ); + expect(sqlBuilderModules.renderSql).toHaveBeenNthCalledWith( + 2, + 'Select', + 'From' + ); + expect(sqlBuilderModules.renderSql).toHaveBeenNthCalledWith( + 3, + 'Select', + 'From' + ); + expect(mockPg.map).toBeCalledTimes(3); + expect(mockPg.map).toHaveBeenNthCalledWith( + 1, + mockSearchQuery, + [], + expect.any(Function) + ); + expect(mockPg.map).toHaveBeenNthCalledWith( + 2, + 'usersRenderSql', + [], + expect.any(Function) + ); + expect(mockPg.map).toHaveBeenNthCalledWith( + 3, + 'privateUsersRenderSql', + [], + expect.any(Function) + ); + expect(profileModules.loadProfiles).toBeCalledTimes(2); + expect(profileModules.loadProfiles).toHaveBeenNthCalledWith( + 1, + mockProps[0] + ); + expect(profileModules.loadProfiles).toHaveBeenNthCalledWith( + 2, + mockProps[1] + ); + expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledTimes(1); + expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledWith({}); + }); + + it('should send search notification emails when there is a matching creator_id entry in private users', async () => { + const mockSearchQuery = "mockSqlQuery"; + const mockSearches = [ + { + created_time: "mockSearchCreatedTime", + creator_id: "mockCreatorId", + id: 123, + last_notified_at: null, + location: {"mockLocation" : "mockLocationValue"}, + search_filters: null, + search_name: null, + }, + { + created_time: "mockCreatedTime1", + creator_id: "mockCreatorId1", + id: 1234, + last_notified_at: null, + location: {"mockLocation1" : "mockLocationValue1"}, + search_filters: null, + search_name: null, + }, + ]; + const _mockUsers = [ + { + created_time: "mockUserCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }, + { + created_time: "mockUserCreatedTime1", + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1", + name: "mockName1", + name_username_vector: "mockNameUsernameVector1", + username: "mockUsername1", + }, + ]; + const mockUsers = { + "user1": { + created_time: "mockUserCreatedTime", + data: {"mockData" : "mockDataValue"}, + id: "mockId", + name: "mockName", + name_username_vector: "mockNameUsernameVector", + username: "mockUsername", + }, + "user2": { + created_time: "mockUserCreatedTime1", + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1", + name: "mockName1", + name_username_vector: "mockNameUsernameVector1", + username: "mockUsername1", + }, + }; + const _mockPrivateUsers = [ + { + data: {"mockData" : "mockDataValue"}, + id: "mockId" + }, + { + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1" + }, + ]; + const mockPrivateUsers = { + "mockCreatorId": { + data: {"mockData" : "mockDataValue"}, + id: "mockId" + }, + "mockCreatorId1": { + data: {"mockData1" : "mockDataValue1"}, + id: "mockId1" + }, + }; + const mockProfiles = [ + { + name: "mockProfileName", + username: "mockProfileUsername" + }, + { + name: "mockProfileName1", + username: "mockProfileUsername1" + }, + ]; + (sqlBuilderModules.renderSql as jest.Mock) + .mockReturnValueOnce(mockSearchQuery) + .mockReturnValueOnce('usersRenderSql') + .mockReturnValueOnce('privateUsersRenderSql'); + (sqlBuilderModules.select as jest.Mock).mockReturnValue('Select'); + (sqlBuilderModules.from as jest.Mock).mockReturnValue('From'); + (mockPg.map as jest.Mock) + .mockResolvedValueOnce(mockSearches) + .mockResolvedValueOnce(_mockUsers) + .mockResolvedValueOnce(_mockPrivateUsers); + (lodashModules.keyBy as jest.Mock) + .mockReturnValueOnce(mockUsers) + .mockReturnValueOnce(mockPrivateUsers); + (profileModules.loadProfiles as jest.Mock) + .mockResolvedValueOnce({profiles: mockProfiles}) + .mockResolvedValueOnce({profiles: mockProfiles}); + jest.spyOn(searchNotificationModules, 'notifyBookmarkedSearch'); + (helperModules.sendSearchAlertsEmail as jest.Mock).mockResolvedValue(null); + + await searchNotificationModules.sendSearchNotifications(); + + expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledTimes(1); + expect(searchNotificationModules.notifyBookmarkedSearch).not.toBeCalledWith({}); + + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/set-compatibility-answers.unit.test.ts b/backend/api/tests/unit/set-compatibility-answers.unit.test.ts new file mode 100644 index 0000000..b0e384e --- /dev/null +++ b/backend/api/tests/unit/set-compatibility-answers.unit.test.ts @@ -0,0 +1,74 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/compatibility/compute-scores'); + +import { setCompatibilityAnswer } from "api/set-compatibility-answer"; +import * as supabaseInit from "shared/supabase/init"; +import { recomputeCompatibilityScoresForUser } from "shared/compatibility/compute-scores"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('setCompatibilityAnswer', () => { + 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 set compatibility answers', async () => { + const mockProps = { + questionId: 1, + multipleChoice: 2, + prefChoices: [1,2,3,4,5], + importance: 1, + explanation: "mockExplanation" + }; + const mockResult = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + explanation: "mockExplanation", + id: 123, + importance: 1, + multipleChoice: 2, + prefChoices: [1,2,3,4,5], + questionId: 1, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.one as jest.Mock).mockResolvedValue(mockResult); + (recomputeCompatibilityScoresForUser as jest.Mock).mockResolvedValue(null); + + const result: any = await setCompatibilityAnswer(mockProps, mockAuth, mockReq); + + expect(result.result).toBe(mockResult); + expect(mockPg.one).toBeCalledTimes(1); + expect(mockPg.one).toBeCalledWith( + { + text: expect.stringContaining('INSERT INTO compatibility_answers'), + values: [ + mockAuth.uid, + mockProps.questionId, + mockProps.multipleChoice, + mockProps.prefChoices, + mockProps.importance, + mockProps.explanation, + ] + } + ); + + await result.continue(); + + expect(recomputeCompatibilityScoresForUser).toBeCalledTimes(1); + expect(recomputeCompatibilityScoresForUser).toBeCalledWith(mockAuth.uid, expect.any(Object)); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/set-last-online-time.unit.test.ts b/backend/api/tests/unit/set-last-online-time.unit.test.ts index 61e8352..36806db 100644 --- a/backend/api/tests/unit/set-last-online-time.unit.test.ts +++ b/backend/api/tests/unit/set-last-online-time.unit.test.ts @@ -1,34 +1,56 @@ jest.mock('shared/supabase/init'); +import { AuthedUser } from "api/helpers/endpoint"; import * as setLastTimeOnlineModule from "api/set-last-online-time"; import * as supabaseInit from "shared/supabase/init"; -describe('Should', () => { +describe('setLastOnlineTimeUser', () => { let mockPg: any; - beforeEach(() => { + jest.resetAllMocks(); mockPg = { none: jest.fn(), }; (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - - jest.clearAllMocks(); + .mockReturnValue(mockPg); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should change the users last online time', async () => { + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockProps = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + jest.spyOn(setLastTimeOnlineModule, 'setLastOnlineTimeUser'); - it('change the users last online time', async () => { - const mockProfile = {user_id: 'Jonathon'}; - - await setLastTimeOnlineModule.setLastOnlineTimeUser(mockProfile.user_id); + await setLastTimeOnlineModule.setLastOnlineTime(mockProps, mockAuth, mockReq); + const [query, userId] = mockPg.none.mock.calls[0]; + + expect(setLastTimeOnlineModule.setLastOnlineTimeUser).toBeCalledTimes(1); + expect(setLastTimeOnlineModule.setLastOnlineTimeUser).toBeCalledWith(mockAuth.uid); + expect(mockPg.none).toBeCalledTimes(1); + expect(userId).toContain(mockAuth.uid); + expect(query).toContain("VALUES ($1, now())"); + expect(query).toContain("ON CONFLICT (user_id)"); + expect(query).toContain("DO UPDATE"); + expect(query).toContain("user_activity.last_online_time < now() - interval '1 minute'"); + }); - expect(mockPg.none).toBeCalledTimes(1); + it('should return if there is no auth', async () => { + const mockAuth = { } as any; + const mockReq = {} as any; + const mockProps = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + jest.spyOn(setLastTimeOnlineModule, 'setLastOnlineTimeUser'); - const [query, userId] = mockPg.none.mock.calls[0]; - - expect(userId).toContain(mockProfile.user_id); - expect(query).toContain("VALUES ($1, now())") - expect(query).toContain("ON CONFLICT (user_id)") - expect(query).toContain("DO UPDATE") - expect(query).toContain("user_activity.last_online_time < now() - interval '1 minute'") + await setLastTimeOnlineModule.setLastOnlineTime(mockProps, mockAuth, mockReq); + + expect(setLastTimeOnlineModule.setLastOnlineTimeUser).not.toBeCalled(); + }); }); -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/backend/api/tests/unit/ship-profiles.unit.test.ts b/backend/api/tests/unit/ship-profiles.unit.test.ts new file mode 100644 index 0000000..397280b --- /dev/null +++ b/backend/api/tests/unit/ship-profiles.unit.test.ts @@ -0,0 +1,227 @@ +jest.mock('shared/supabase/init'); +jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/utils'); +jest.mock('shared/create-profile-notification'); + +import { shipProfiles } from "api/ship-profiles"; +import * as supabaseInit from "shared/supabase/init"; +import { tryCatch } from "common/util/try-catch"; +import * as supabaseUtils from "shared/supabase/utils"; +import * as profileNotificationModules from "shared/create-profile-notification"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('shipProfiles', () => { + 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 return success if the profile ship already exists', async () => { + const mockProps = { + targetUserId1: "mockTargetUserId1", + targetUserId2: "mockTargetUserId2", + remove: false, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + data: { ship_id : "mockShipId" }, + error: null + }; + + (tryCatch as jest.Mock).mockResolvedValue(mockExisting); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + + const result: any = await shipProfiles(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(tryCatch).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select ship_id from profile_ships'), + [mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2] + ); + }); + + it('should return success if trying to remove a profile ship that already exists', async () => { + const mockProps = { + targetUserId1: "mockTargetUserId1", + targetUserId2: "mockTargetUserId2", + remove: true, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + data: { ship_id : "mockShipId" }, + error: null + }; + + (tryCatch as jest.Mock) + .mockResolvedValueOnce(mockExisting) + .mockResolvedValueOnce({error: null}); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (mockPg.none as jest.Mock).mockResolvedValue(null); + + const result: any = await shipProfiles(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(tryCatch).toBeCalledTimes(2); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select ship_id from profile_ships'), + [mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('delete from profile_ships where ship_id = $1'), + [mockExisting.data.ship_id] + ); + }); + + it('should return success when creating a new profile ship', async () => { + const mockProps = { + targetUserId1: "mockTargetUserId1", + targetUserId2: "mockTargetUserId2", + remove: false, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + data: null, + error: null + }; + const mockData = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + ship_id: "mockShipId", + target1_id: "mockTarget1Id", + target2_id: "mockTarget2Id", + }; + + (tryCatch as jest.Mock) + .mockResolvedValueOnce(mockExisting) + .mockResolvedValueOnce({data: mockData, error: null}); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + + const result: any = await shipProfiles(mockProps, mockAuth, mockReq); + + expect(result.result.status).toBe('success'); + expect(tryCatch).toBeCalledTimes(2); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select ship_id from profile_ships'), + [mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2] + ); + expect(supabaseUtils.insert).toBeCalledTimes(1); + expect(supabaseUtils.insert).toBeCalledWith( + expect.any(Object), + 'profile_ships', + { + creator_id: mockAuth.uid, + target1_id: mockProps.targetUserId1, + target2_id: mockProps.targetUserId2, + } + ); + + (profileNotificationModules.createProfileShipNotification as jest.Mock).mockReturnValue(null); + + await result.continue(); + + expect(profileNotificationModules.createProfileShipNotification).toBeCalledTimes(2); + expect(profileNotificationModules.createProfileShipNotification).toHaveBeenNthCalledWith( + 1, + mockData, + mockData.target1_id + ); + expect(profileNotificationModules.createProfileShipNotification).toHaveBeenNthCalledWith( + 2, + mockData, + mockData.target2_id + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if unable to check ship', async () => { + const mockProps = { + targetUserId1: "mockTargetUserId1", + targetUserId2: "mockTargetUserId2", + remove: false, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + data: null, + error: Error + }; + + (tryCatch as jest.Mock).mockResolvedValue(mockExisting); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + + expect(shipProfiles(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Error when checking ship: '); + + }); + + it('should throw if unable to remove a profile ship that already exists', async () => { + const mockProps = { + targetUserId1: "mockTargetUserId1", + targetUserId2: "mockTargetUserId2", + remove: true, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + data: { ship_id : "mockShipId" }, + error: null + }; + + (tryCatch as jest.Mock) + .mockResolvedValueOnce(mockExisting) + .mockResolvedValueOnce({error: Error}); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (mockPg.none as jest.Mock).mockResolvedValue(null); + + expect(shipProfiles(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Failed to remove ship: '); + }); + + it('should return success when creating a new profile ship', async () => { + const mockProps = { + targetUserId1: "mockTargetUserId1", + targetUserId2: "mockTargetUserId2", + remove: false, + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + data: null, + error: null + }; + + (tryCatch as jest.Mock) + .mockResolvedValueOnce(mockExisting) + .mockResolvedValueOnce({data: null, error: Error}); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + + expect(shipProfiles(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Failed to create ship: '); + + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/star-profile.unit.test.ts b/backend/api/tests/unit/star-profile.unit.test.ts new file mode 100644 index 0000000..3708918 --- /dev/null +++ b/backend/api/tests/unit/star-profile.unit.test.ts @@ -0,0 +1,146 @@ +jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/utils'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { starProfile } from "api/star-profile"; +import { tryCatch } from "common/util/try-catch"; +import * as supabaseInit from "shared/supabase/init"; +import * as supabaseUtils from "shared/supabase/utils"; + +describe('startProfile', () => { + let mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockPg = { + none: jest.fn(), + oneOrNone: jest.fn(), + }; + + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should return success when trying to star a profile for the first time', 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) + .mockResolvedValueOnce({data: null}) + .mockResolvedValueOnce({error: null}); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + + const result: any = await starProfile(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(tryCatch).toBeCalledTimes(2); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select * from profile_stars where creator_id = $1 and target_id = $2'), + [mockAuth.uid, mockProps.targetUserId] + ); + expect(supabaseUtils.insert).toBeCalledTimes(1); + expect(supabaseUtils.insert).toBeCalledWith( + expect.any(Object), + 'profile_stars', + { + creator_id: mockAuth.uid, + target_id: mockProps.targetUserId + } + ); + }); + + it('should return success if the profile already has a star', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockExisting = { + created_time: "mockCreatedTime", + creator_id: "mockCreatorId", + star_id: "mockStarId", + target_id: "mockTarget", + }; + + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({data: mockExisting}); + + const result: any = await starProfile(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(tryCatch).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(supabaseUtils.insert).not.toBeCalledTimes(1); + }); + + it('should return success when trying to remove a star', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValue({error: null}); + + const result: any = await starProfile(mockProps, mockAuth, mockReq); + + expect(result.status).toBe('success'); + expect(tryCatch).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('delete from profile_stars where creator_id = $1 and target_id = $2'), + [mockAuth.uid, mockProps.targetUserId] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if unable to remove star', async () => { + const mockProps = { + targetUserId: "mockTargetUserId", + remove: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + (tryCatch as jest.Mock).mockResolvedValueOnce({error: Error}); + + expect(starProfile(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Failed to remove star'); + }); + + it('should throw if unable to add a star', 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) + .mockResolvedValueOnce({data: null}) + .mockResolvedValueOnce({error: Error}); + (supabaseUtils.insert as jest.Mock).mockReturnValue(null); + + expect(starProfile(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Failed to add star'); + + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/update-me.unit.test.ts b/backend/api/tests/unit/update-me.unit.test.ts new file mode 100644 index 0000000..cd0c104 --- /dev/null +++ b/backend/api/tests/unit/update-me.unit.test.ts @@ -0,0 +1,255 @@ +jest.mock('common/api/user-types'); +jest.mock('common/util/clean-username'); +jest.mock('shared/supabase/init'); +jest.mock('common/util/object'); +jest.mock('lodash'); +jest.mock('shared/utils'); +jest.mock('shared/supabase/users'); +jest.mock('shared/websockets/helpers'); +jest.mock('common/envs/constants'); + +import { updateMe } from "api/update-me"; +import { toUserAPIResponse } from "common/api/user-types"; +import * as cleanUsernameModules from "common/util/clean-username"; +import * as supabaseInit from "shared/supabase/init"; +import * as objectUtils from "common/util/object"; +import * as lodashModules from "lodash"; +import * as sharedUtils from "shared/utils"; +import * as supabaseUsers from "shared/supabase/users"; +import * as websocketHelperModules from "shared/websockets/helpers"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('updateMe', () => { + 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 update user information', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUpdate = { + name: "mockName", + username: "mockUsername", + avatarUrl: "mockAvatarUrl", + bio: "mockBio", + link: {"mockLink" : "mockLinkValue"}, + optOutBetWarnings:true, + website: "mockWebsite", + twitterHandle: "mockTwitterHandle", + discordHandle: "mockDiscordHandle", + }; + const mockStripped = { + bio: "mockBio" + }; + const mockData = {link: "mockNewLinks"}; + const arrySpy = jest.spyOn(Array.prototype, 'includes'); + + (lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + (cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name); + (cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(mockUpdate.username); + arrySpy.mockReturnValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(false); + (supabaseUsers.updateUser as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (lodashModules.mapValues as jest.Mock).mockReturnValue(mockStripped); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockData); + (mockPg.none as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + (objectUtils.removeUndefinedProps as jest.Mock).mockReturnValue("mockRemoveUndefinedProps"); + (websocketHelperModules.broadcastUpdatedUser as jest.Mock).mockReturnValue(null); + (toUserAPIResponse as jest.Mock).mockReturnValue(null); + + await updateMe(mockProps, mockAuth, mockReq); + + expect(lodashModules.cloneDeep).toBeCalledTimes(1); + expect(lodashModules.cloneDeep).toBeCalledWith(mockProps); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(cleanUsernameModules.cleanDisplayName).toBeCalledTimes(1); + expect(cleanUsernameModules.cleanDisplayName).toBeCalledWith(mockUpdate.name); + expect(cleanUsernameModules.cleanUsername).toBeCalledTimes(1); + expect(cleanUsernameModules.cleanUsername).toBeCalledWith(mockUpdate.username); + expect(arrySpy).toBeCalledTimes(1); + expect(arrySpy).toBeCalledWith(mockUpdate.username); + expect(sharedUtils.getUserByUsername).toBeCalledTimes(1); + expect(sharedUtils.getUserByUsername).toBeCalledWith(mockUpdate.username); + expect(supabaseUsers.updateUser).toBeCalledTimes(2); + expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + mockAuth.uid, + 'mockRemoveUndefinedProps' + ); + expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + mockAuth.uid, + {avatarUrl: mockUpdate.avatarUrl} + ); + expect(lodashModules.mapValues).toBeCalledTimes(1); + expect(lodashModules.mapValues).toBeCalledWith( + expect.any(Object), + expect.any(Function) + ); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('update users'), + { + adds: expect.any(Object), + removes: expect.any(Array), + id: mockAuth.uid + } + ); + expect(mockPg.none).toBeCalledTimes(2); + expect(mockPg.none).toHaveBeenNthCalledWith( + 1, + expect.stringContaining(`update users set name = $1 where id = $2`), + [mockUpdate.name, mockAuth.uid] + ); + expect(mockPg.none).toHaveBeenNthCalledWith( + 2, + expect.stringContaining(`update users set username = $1 where id = $2`), + [mockUpdate.username, mockAuth.uid] + ); + expect(objectUtils.removeUndefinedProps).toBeCalledTimes(2); + expect(objectUtils.removeUndefinedProps).toHaveBeenNthCalledWith( + 2, + { + id: mockAuth.uid, + name: mockUpdate.name, + username: mockUpdate.username, + avatarUrl: mockUpdate.avatarUrl, + link: mockData.link + } + ); + expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledTimes(1); + expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledWith('mockRemoveUndefinedProps'); + expect(toUserAPIResponse).toBeCalledTimes(1); + }); + }); + describe('when an error occurs', () => { + it('should throw if no account was found', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUpdate = { + name: "mockName", + username: "mockUsername", + avatarUrl: "mockAvatarUrl", + bio: "mockBio", + link: {"mockLink" : "mockLinkValue"}, + optOutBetWarnings:true, + website: "mockWebsite", + twitterHandle: "mockTwitterHandle", + discordHandle: "mockDiscordHandle", + }; + + (lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + + expect(updateMe(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Your account was not found'); + }); + + it('should throw if the username is invalid', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUpdate = { + name: "mockName", + username: "mockUsername", + avatarUrl: "mockAvatarUrl", + bio: "mockBio", + link: {"mockLink" : "mockLinkValue"}, + optOutBetWarnings:true, + website: "mockWebsite", + twitterHandle: "mockTwitterHandle", + discordHandle: "mockDiscordHandle", + }; + + (lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + (cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name); + (cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(false); + + expect(updateMe(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Invalid username'); + }); + + it('should throw if the username is reserved', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUpdate = { + name: "mockName", + username: "mockUsername", + avatarUrl: "mockAvatarUrl", + bio: "mockBio", + link: {"mockLink" : "mockLinkValue"}, + optOutBetWarnings:true, + website: "mockWebsite", + twitterHandle: "mockTwitterHandle", + discordHandle: "mockDiscordHandle", + }; + + const arrySpy = jest.spyOn(Array.prototype, 'includes'); + + (lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + (cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name); + (cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(mockUpdate.username); + arrySpy.mockReturnValue(true); + + expect(updateMe(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('This username is reserved'); + }); + + it('should throw if the username is taken', async () => { + const mockProps = {} as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUpdate = { + name: "mockName", + username: "mockUsername", + avatarUrl: "mockAvatarUrl", + bio: "mockBio", + link: {"mockLink" : "mockLinkValue"}, + optOutBetWarnings:true, + website: "mockWebsite", + twitterHandle: "mockTwitterHandle", + discordHandle: "mockDiscordHandle", + }; + const arrySpy = jest.spyOn(Array.prototype, 'includes'); + + (lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate); + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + (cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name); + (cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(mockUpdate.username); + arrySpy.mockReturnValue(false); + (sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(true); + + expect(updateMe(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Username already taken'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/update-notif-setting.unit.test.ts b/backend/api/tests/unit/update-notif-setting.unit.test.ts new file mode 100644 index 0000000..526e7b9 --- /dev/null +++ b/backend/api/tests/unit/update-notif-setting.unit.test.ts @@ -0,0 +1,71 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/users'); +jest.mock('shared/websockets/helpers'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { updateNotifSettings } from "api/update-notif-setting"; +import * as supabaseInit from "shared/supabase/init"; +import * as supabaseUsers from "shared/supabase/users"; +import * as websocketHelpers from "shared/websockets/helpers"; + +describe('updateNotifSettings', () => { + 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 update notification settings', async () => { + const mockProps = { + type: "new_match" as const, + medium: "email" as const, + enabled: false + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (mockPg.none as jest.Mock).mockResolvedValue(null); + (websocketHelpers.broadcastUpdatedPrivateUser as jest.Mock).mockReturnValue(null); + + await updateNotifSettings(mockProps, mockAuth, mockReq); + + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('update private_users'), + [mockProps.type, mockProps.medium, mockAuth.uid] + ); + expect(websocketHelpers.broadcastUpdatedPrivateUser).toBeCalledTimes(1); + expect(websocketHelpers.broadcastUpdatedPrivateUser).toBeCalledWith(mockAuth.uid); + }); + + it('should turn off notifications', async () => { + const mockProps = { + type: "opt_out_all" as const, + medium: "mobile" as const, + enabled: true + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null); + + await updateNotifSettings(mockProps, mockAuth, mockReq); + + expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(1); + expect(supabaseUsers.updatePrivateUser).toBeCalledWith( + expect.any(Object), + mockAuth.uid, + {interestedInPushNotifications: !mockProps.enabled} + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/update-options.unit.test.ts b/backend/api/tests/unit/update-options.unit.test.ts new file mode 100644 index 0000000..fe985bc --- /dev/null +++ b/backend/api/tests/unit/update-options.unit.test.ts @@ -0,0 +1,170 @@ +jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/init'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { updateOptions } from "api/update-options"; +import { tryCatch } from "common/util/try-catch"; +import * as supabaseInit from "shared/supabase/init"; + +describe('updateOptions', () => { + let mockPg = {} as any; + let mockTx = {} as any; + beforeEach(() => { + jest.resetAllMocks(); + mockTx = { + one: jest.fn(), + none: jest.fn() + }; + mockPg = { + oneOrNone: jest.fn(), + tx: jest.fn(async (cb) => await cb(mockTx)) + }; + (supabaseInit.createSupabaseDirectClient as jest.Mock) + .mockReturnValue(mockPg); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when given valid input', () => { + it('should update user', async () => { + const mockProps = { + table: 'causes' as const, + names: ["mockNamesOne", "mockNamesTwo"] + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockProfileIdResult = {id: 123}; + const mockRow1 = { + id: 1234, + }; + const mockRow2 = { + id: 12345, + }; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockProfileIdResult); + (tryCatch as jest.Mock).mockImplementation(async (fn: any) => { + try { + const data = await fn; + return {data, error: null}; + } catch (error) { + return {data:null, error}; + } + }); + (mockTx.one as jest.Mock) + .mockResolvedValueOnce(mockRow1) + .mockResolvedValueOnce(mockRow2); + + const result: any = await updateOptions(mockProps, mockAuth, mockReq); + + expect(result.updatedIds).toStrictEqual([mockRow1.id, mockRow2.id]); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('SELECT id FROM profiles WHERE user_id = $1'), + [mockAuth.uid] + ); + expect(tryCatch).toBeCalledTimes(1); + expect(mockTx.one).toBeCalledTimes(2); + expect(mockTx.one).toHaveBeenNthCalledWith( + 1, + expect.stringContaining(`INSERT INTO ${mockProps.table} (name, creator_id)`), + [mockProps.names[0], mockAuth.uid] + ); + expect(mockTx.one).toHaveBeenNthCalledWith( + 2, + expect.stringContaining(`INSERT INTO ${mockProps.table} (name, creator_id)`), + [mockProps.names[1], mockAuth.uid] + ); + expect(mockTx.none).toBeCalledTimes(2); + expect(mockTx.none).toHaveBeenNthCalledWith( + 1, + expect.stringContaining(`DELETE FROM profile_${mockProps.table} WHERE profile_id = $1`), + [mockProfileIdResult.id] + ); + expect(mockTx.none).toHaveBeenNthCalledWith( + 2, + expect.stringContaining(`INSERT INTO profile_${mockProps.table} (profile_id, option_id) VALUES`), + [mockProfileIdResult.id, mockRow1.id, mockRow2.id] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if the table param is invalid', async () => { + const mockProps = { + table: 'causes' as const, + names: ["mockNamesOne", "mockNamesTwo"] + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(false); + + expect(updateOptions(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Invalid table'); + }); + + it('should throw if the names param is not provided', async () => { + const mockProps = { + table: 'causes' as const, + names: [] + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + + expect(updateOptions(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('No names provided'); + }); + + it('should throw if unable to find profile', async () => { + const mockProps = { + table: 'causes' as const, + names: ["mockNamesOne", "mockNamesTwo"] + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + expect(updateOptions(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Profile not found'); + }); + + it('should update user', async () => { + const mockProps = { + table: 'causes' as const, + names: ["mockNamesOne", "mockNamesTwo"] + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockProfileIdResult = {id: 123}; + const mockRow1 = { + id: 1234, + }; + const mockRow2 = { + id: 12345, + }; + + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockProfileIdResult); + (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + (mockPg.tx as jest.Mock).mockResolvedValue(null); + (mockTx.one as jest.Mock) + .mockResolvedValueOnce(mockRow1) + .mockResolvedValueOnce(mockRow2); + (mockTx.none as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + expect(updateOptions(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Error updating profile options'); + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts new file mode 100644 index 0000000..01dfd7f --- /dev/null +++ b/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts @@ -0,0 +1,91 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/utils'); +jest.mock('common/supabase/utils'); + +import {updatePrivateUserMessageChannel} from "api/update-private-user-message-channel"; +import * as supabaseInit from "shared/supabase/init"; +import * as sharedUtils from "shared/utils"; +import * as supabaseUtils from "common/supabase/utils"; +import { AuthedUser } from "api/helpers/endpoint"; + +describe('updatePrivateUserMessageChannel', () => { + 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 return success after updating the users private message channel', async () => { + const mockBody = { + channelId: 123, + notifyAfterTime: 10 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(true); + (supabaseUtils.millisToTs as jest.Mock).mockReturnValue('mockMillisToTs'); + + const results = await updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq); + + expect(results.status).toBe('success'); + expect(results.channelId).toBe(mockBody.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'), + [mockBody.channelId, mockAuth.uid] + ); + expect(mockPg.none).toBeCalledTimes(1); + expect(mockPg.none).toBeCalledWith( + expect.stringContaining('update private_user_message_channel_members'), + [mockBody.channelId, mockAuth.uid, 'mockMillisToTs'] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if the user account does not exist', async () => { + const mockBody = { + channelId: 123, + notifyAfterTime: 10 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + + expect(updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('Your account was not found'); + }); + + it('should throw if the user is not authorized in the channel', async () => { + const mockBody = { + channelId: 123, + notifyAfterTime: 10 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(true); + (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + + expect(updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq)) + .rejects + .toThrow('You are not authorized to this channel'); + + }); + }); +}); \ No newline at end of file diff --git a/backend/api/tests/unit/update-profile.unit.test.ts b/backend/api/tests/unit/update-profile.unit.test.ts index 6225fe4..4807942 100644 --- a/backend/api/tests/unit/update-profile.unit.test.ts +++ b/backend/api/tests/unit/update-profile.unit.test.ts @@ -1,86 +1,100 @@ jest.mock("shared/supabase/init"); jest.mock("shared/supabase/utils"); +jest.mock("common/util/try-catch"); +jest.mock("shared/profiles/parse-photos"); +jest.mock("shared/supabase/users"); -import { AuthedUser } from "api/helpers/endpoint"; import { updateProfile } from "api/update-profile"; +import { AuthedUser } from "api/helpers/endpoint"; import * as supabaseInit from "shared/supabase/init"; import * as supabaseUtils from "shared/supabase/utils"; +import * as supabaseUsers from "shared/supabase/users"; +import { tryCatch } from "common/util/try-catch"; +import { removePinnedUrlFromPhotoUrls } from "shared/profiles/parse-photos"; describe('updateProfiles', () => { let mockPg: any; - beforeEach(() => { + jest.resetAllMocks(); mockPg = { oneOrNone: jest.fn(), }; (supabaseInit.createSupabaseDirectClient as jest.Mock) .mockReturnValue(mockPg); - - jest.clearAllMocks(); }); - describe('should', () => { - it('update an existing profile when provided the user id', async () => { - const mockUserProfile = { - user_id: '234', - diet: 'Nothing', - gender: 'female', - is_smoker: true, - } - const mockUpdateMade = { - gender: 'male' - } - const mockUpdatedProfile = { - user_id: '234', - diet: 'Nothing', - gender: 'male', - is_smoker: true, - } - const mockParams = {} as any; - const mockAuth = { - uid: '234' - } + afterEach(() => { + jest.restoreAllMocks(); + }); - mockPg.oneOrNone.mockResolvedValue(mockUserProfile); - (supabaseUtils.update as jest.Mock).mockResolvedValue(mockUpdatedProfile); + describe('when given valid input', () => { + it('should update an existing profile when provided the user id', async () => { + const mockProps = { + avatar_url: "mockAvatarUrl" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockData = "success"; - const result = await updateProfile( - mockUpdateMade, - mockAuth as AuthedUser, - mockParams - ); + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: true}) + .mockResolvedValueOnce({data: mockData, error: null}); - expect(mockPg.oneOrNone.mock.calls.length).toBe(1); - expect(mockPg.oneOrNone.mock.calls[0][1]).toEqual([mockAuth.uid]); - expect(result).toEqual(mockUpdatedProfile); + const result = await updateProfile(mockProps, mockAuth, mockReq); + + expect(result).toBe(mockData); + expect(mockPg.oneOrNone).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledWith( + expect.stringContaining('select * from profiles where user_id = $1'), + [mockAuth.uid] + ); + expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1); + expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockProps); + expect(supabaseUsers.updateUser).toBeCalledTimes(1); + expect(supabaseUsers.updateUser).toBeCalledWith( + expect.any(Object), + mockAuth.uid, + {avatarUrl: mockProps.avatar_url} + ); + expect(supabaseUtils.update).toBeCalledTimes(1); + expect(supabaseUtils.update).toBeCalledWith( + expect.any(Object), + 'profiles', + 'user_id', + expect.any(Object) + ); }); + }); + + describe('when an error occurs', () => { + it('should throw if the profile does not exist', async () => { + const mockProps = { + avatar_url: "mockAvatarUrl" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; - it('throw an error if a profile is not found', async () => { - mockPg.oneOrNone.mockResolvedValue(null); - expect(updateProfile({} as any, {} as any, {} as any,)) + (tryCatch as jest.Mock).mockResolvedValue({data: false}); + + expect(updateProfile(mockProps, mockAuth, mockReq)) .rejects - .toThrowError('Profile not found'); + .toThrow('Profile not found'); }); - it('throw an error if unable to update the profile', async () => { - const mockUserProfile = { - user_id: '234', - diet: 'Nothing', - gender: 'female', - is_smoker: true, - } - const data = null; - const error = true; - const mockError = { - data, - error - } - mockPg.oneOrNone.mockResolvedValue(mockUserProfile); - (supabaseUtils.update as jest.Mock).mockRejectedValue(mockError); - expect(updateProfile({} as any, {} as any, {} as any,)) + it('should throw if unable to update the profile', async () => { + const mockProps = { + avatar_url: "mockAvatarUrl" + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (tryCatch as jest.Mock) + .mockResolvedValueOnce({data: true}) + .mockResolvedValueOnce({data: null, error: Error}); + + expect(updateProfile(mockProps, mockAuth, mockReq)) .rejects - .toThrowError('Error updating profile'); - + .toThrow('Error updating profile'); }); }); }); \ No newline at end of file diff --git a/backend/api/tests/unit/vote-unit.test.ts b/backend/api/tests/unit/vote-unit.test.ts new file mode 100644 index 0000000..25f2358 --- /dev/null +++ b/backend/api/tests/unit/vote-unit.test.ts @@ -0,0 +1,101 @@ +jest.mock('shared/supabase/init'); +jest.mock('shared/utils'); + +import { AuthedUser } from "api/helpers/endpoint"; +import { vote } from "api/vote"; +import * as supabaseInit from "shared/supabase/init"; +import * as sharedUtils from "shared/utils"; + +describe('vote', () => { + 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 vote successfully', async () => { + const mockProps = { + voteId: 1, + choice: 'for' as const, + priority: 10 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUser = {id: "mockUserId"}; + const mockResult = "success"; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (mockPg.one as jest.Mock).mockResolvedValue(mockResult); + + const result = await vote(mockProps, mockAuth, mockReq); + + expect(result.data).toBe(mockResult); + expect(sharedUtils.getUser).toBeCalledTimes(1); + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); + expect(mockPg.one).toBeCalledTimes(1); + expect(mockPg.one).toBeCalledWith( + expect.stringContaining('insert into vote_results (user_id, vote_id, choice, priority)'), + [mockUser.id, mockProps.voteId, 1, mockProps.priority] + ); + }); + }); + describe('when an error occurs', () => { + it('should throw if unable to find the account', async () => { + const mockProps = { + voteId: 1, + choice: 'for' as const, + priority: 10 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + + expect(vote(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Your account was not found'); + }); + + it('should throw if the choice is invalid', async () => { + const mockProps = { + voteId: 1, + priority: 10 + } as any; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUser = {id: "mockUserId"}; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + + expect(vote(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Invalid choice'); + }); + + it('should throw if unable to record vote', async () => { + const mockProps = { + voteId: 1, + choice: 'for' as const, + priority: 10 + }; + const mockAuth = { uid: '321' } as AuthedUser; + const mockReq = {} as any; + const mockUser = {id: "mockUserId"}; + + (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + (mockPg.one as jest.Mock).mockRejectedValue(new Error('Result error')); + + expect(vote(mockProps, mockAuth, mockReq)) + .rejects + .toThrow('Error recording vote'); + }); + }); +}); \ No newline at end of file