From 48df0de9b9a6d1859fe1411e7154f201fff64ffe Mon Sep 17 00:00:00 2001 From: "Xunnamius (Romulus)" Date: Sun, 4 Jul 2021 00:51:43 -0700 Subject: [PATCH] test: all api (not backend) unit tests passing --- lib/next-use-redirection/README.md | 2 +- src/pages/api/v1/memes/[...meme_ids].ts | 3 +- src/pages/api/v1/memes/index.ts | 3 +- src/pages/api/v1/memes/search.ts | 1 + .../v1/users/[user_id]/friends/[friend_id].ts | 18 +- .../api/v1/users/[user_id]/friends/index.ts | 10 +- src/pages/api/v1/users/[user_id]/index.ts | 18 +- .../api/v1/users/[user_id]/liked/[meme_id].ts | 1 + .../api/v1/users/[user_id]/liked/index.ts | 6 +- .../requests/[request_type]/[target_id].ts | 31 +- .../requests/[request_type]/index.ts | 20 +- src/pages/api/v1/users/index.ts | 7 +- test/integration.test.ts | 3 + test/unit-api-memes.test.ts | 6 +- test/unit-api-users.test.ts | 469 +++++++----------- 15 files changed, 270 insertions(+), 328 deletions(-) diff --git a/lib/next-use-redirection/README.md b/lib/next-use-redirection/README.md index 3e2685f..f0febb9 100644 --- a/lib/next-use-redirection/README.md +++ b/lib/next-use-redirection/README.md @@ -16,7 +16,7 @@ import * as React from 'react' import { useRedirection } from 'next-use-redirection' import { useUser } from 'universe/frontend/hooks' import PasswordForm from 'components/password-form' -import { WithAuthed, User } from 'types/global'; +import type { WithAuthed, User } from 'types/global'; const REDIRECT_ON_NOT_FIRST_LOGIN_LOCATION = '/dashboard'; diff --git a/src/pages/api/v1/memes/[...meme_ids].ts b/src/pages/api/v1/memes/[...meme_ids].ts index 0668edb..06b0026 100644 --- a/src/pages/api/v1/memes/[...meme_ids].ts +++ b/src/pages/api/v1/memes/[...meme_ids].ts @@ -31,8 +31,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { } else sendHttpOk(res, { memes }); } else { // * PUT - // TODO: validation - await updateMemes({ meme_ids, data: {} }); + await updateMemes({ meme_ids, data: req.body }); sendHttpOk(res); } }, diff --git a/src/pages/api/v1/memes/index.ts b/src/pages/api/v1/memes/index.ts index f33d184..5ee1954 100644 --- a/src/pages/api/v1/memes/index.ts +++ b/src/pages/api/v1/memes/index.ts @@ -30,7 +30,8 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { regexMatch: {} }) }); - } else + } // * POST + else sendHttpOk(res, { meme: await createMeme({ creatorKey: key, data: req.body }) }); }, { req, res, methods: ['GET', 'POST'], apiVersion: 1 } diff --git a/src/pages/api/v1/memes/search.ts b/src/pages/api/v1/memes/search.ts index 8f15be2..a097481 100644 --- a/src/pages/api/v1/memes/search.ts +++ b/src/pages/api/v1/memes/search.ts @@ -31,6 +31,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { } if (match && regexMatch) { + // * GET sendHttpOk(res, { memes: await searchMemes({ after, diff --git a/src/pages/api/v1/users/[user_id]/friends/[friend_id].ts b/src/pages/api/v1/users/[user_id]/friends/[friend_id].ts index 9625a7c..d5ce879 100644 --- a/src/pages/api/v1/users/[user_id]/friends/[friend_id].ts +++ b/src/pages/api/v1/users/[user_id]/friends/[friend_id].ts @@ -1,10 +1,11 @@ import { wrapHandler } from 'universe/backend/middleware'; -import { addFriend, isUserAFriend, removePackmate } from 'universe/backend'; +import { addUserAsFriend, isUserAFriend, removeUserAsFriend } from 'universe/backend'; import { sendHttpNotFound, sendHttpOk } from 'multiverse/next-respond'; import { ValidationError } from 'universe/backend/error'; import { ObjectId } from 'mongodb'; import type { NextApiResponse, NextApiRequest } from 'next'; +import type { FriendId, UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -12,14 +13,14 @@ export { defaultConfig as config } from 'universe/backend/middleware'; export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { - let packmate_id: ObjectId | undefined = undefined; - let user_id: ObjectId | undefined = undefined; + let friend_id: FriendId | undefined = undefined; + let user_id: UserId | undefined = undefined; try { - packmate_id = new ObjectId(req.query.packmate_id.toString()); + friend_id = new ObjectId(req.query.friend_id.toString()); } catch { throw new ValidationError( - `invalid packmate_id "${req.query.packmate_id.toString()}"` + `invalid friend_id "${req.query.friend_id.toString()}"` ); } @@ -30,14 +31,15 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { } if (req.method == 'GET') { - (await isUserAFriend({ packmate_id, user_id })) + (await isUserAFriend({ friend_id, user_id })) ? sendHttpOk(res) : sendHttpNotFound(res); } else if (req.method == 'DELETE') { - await removePackmate({ packmate_id, user_id }); + await removeUserAsFriend({ friend_id, user_id }); sendHttpOk(res); } else { - await addFriend({ packmate_id, user_id }); + // * PUT + await addUserAsFriend({ friend_id, user_id }); sendHttpOk(res); } }, diff --git a/src/pages/api/v1/users/[user_id]/friends/index.ts b/src/pages/api/v1/users/[user_id]/friends/index.ts index 00f018a..c301f11 100644 --- a/src/pages/api/v1/users/[user_id]/friends/index.ts +++ b/src/pages/api/v1/users/[user_id]/friends/index.ts @@ -1,10 +1,11 @@ -import { getRequestsOfType } from 'universe/backend'; +import { getUserFriendsUserIds } from 'universe/backend'; import { sendHttpOk } from 'multiverse/next-respond'; import { wrapHandler } from 'universe/backend/middleware'; import { ValidationError } from 'universe/backend/error'; import { ObjectId } from 'mongodb'; import type { NextApiResponse, NextApiRequest } from 'next'; +import type { UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -12,8 +13,8 @@ export { defaultConfig as config } from 'universe/backend/middleware'; export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { - let after: ObjectId | null | undefined = undefined; - let user_id: ObjectId | undefined = undefined; + let after: UserId | null | undefined = undefined; + let user_id: UserId | undefined = undefined; try { after = req.query.after ? new ObjectId(req.query.after.toString()) : null; @@ -27,8 +28,9 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); } + // * GET sendHttpOk(res, { - users: await getRequestsOfType({ user_id, after }) + users: await getUserFriendsUserIds({ user_id, after }) }); }, { diff --git a/src/pages/api/v1/users/[user_id]/index.ts b/src/pages/api/v1/users/[user_id]/index.ts index cac2dfc..f769cf6 100644 --- a/src/pages/api/v1/users/[user_id]/index.ts +++ b/src/pages/api/v1/users/[user_id]/index.ts @@ -2,9 +2,10 @@ import { getUser, deleteUser, updateUser } from 'universe/backend'; import { sendHttpOk } from 'multiverse/next-respond'; import { wrapHandler } from 'universe/backend/middleware'; import { ObjectId } from 'mongodb'; +import { GuruMeditationError, ValidationError } from 'universe/backend/error'; import type { NextApiResponse, NextApiRequest } from 'next'; -import { ValidationError } from 'universe/backend/error'; +import type { UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -12,20 +13,27 @@ export { defaultConfig as config } from 'universe/backend/middleware'; export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { - let user_id: ObjectId | undefined = undefined; + const param = req.query.user_id; + let user_id: UserId | undefined = undefined; + let username: string | undefined = undefined; try { - user_id = new ObjectId(req.query.user_id.toString()); + user_id = new ObjectId(param.toString()); } catch { - throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); + if (req.method != 'GET' || typeof param != 'string' || !param.length) + throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); + else username = param; } if (req.method == 'GET') { - sendHttpOk(res, { user: await getUser({ user_id }) }); + sendHttpOk(res, { user: await getUser(user_id ? { user_id } : { username }) }); + } else if (!user_id) { + throw new GuruMeditationError('sanity check failed: user_id is missing'); } else if (req.method == 'DELETE') { await deleteUser({ user_id }); sendHttpOk(res); } else { + // * PUT await updateUser({ user_id, data: req.body }); sendHttpOk(res); } diff --git a/src/pages/api/v1/users/[user_id]/liked/[meme_id].ts b/src/pages/api/v1/users/[user_id]/liked/[meme_id].ts index 46a2a2a..14df73c 100644 --- a/src/pages/api/v1/users/[user_id]/liked/[meme_id].ts +++ b/src/pages/api/v1/users/[user_id]/liked/[meme_id].ts @@ -28,6 +28,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); } + // * GET if (user_id !== undefined) { (await isMemeLiked({ meme_id, user_id })) ? sendHttpOk(res) diff --git a/src/pages/api/v1/users/[user_id]/liked/index.ts b/src/pages/api/v1/users/[user_id]/liked/index.ts index 85747a1..3c3937c 100644 --- a/src/pages/api/v1/users/[user_id]/liked/index.ts +++ b/src/pages/api/v1/users/[user_id]/liked/index.ts @@ -5,6 +5,7 @@ import { ValidationError } from 'universe/backend/error'; import { ObjectId } from 'mongodb'; import type { NextApiResponse, NextApiRequest } from 'next'; +import type { MemeId, UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -12,8 +13,8 @@ export { defaultConfig as config } from 'universe/backend/middleware'; export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { - let after: ObjectId | null | undefined = undefined; - let user_id: ObjectId | undefined = undefined; + let after: MemeId | null | undefined = undefined; + let user_id: UserId | undefined = undefined; try { after = req.query.after ? new ObjectId(req.query.after.toString()) : null; @@ -27,6 +28,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); } + // * GET sendHttpOk(res, { memes: await getUserLikedMemeIds({ user_id, after }) }); }, { diff --git a/src/pages/api/v1/users/[user_id]/requests/[request_type]/[target_id].ts b/src/pages/api/v1/users/[user_id]/requests/[request_type]/[target_id].ts index 1fdbc83..475b93d 100644 --- a/src/pages/api/v1/users/[user_id]/requests/[request_type]/[target_id].ts +++ b/src/pages/api/v1/users/[user_id]/requests/[request_type]/[target_id].ts @@ -1,10 +1,15 @@ import { wrapHandler } from 'universe/backend/middleware'; -import { addFriendRequest, isUserFollowing, unfollowUser } from 'universe/backend'; +import { + addFriendRequest, + isFriendRequestOfType, + removeFriendRequest +} from 'universe/backend'; import { sendHttpNotFound, sendHttpOk } from 'multiverse/next-respond'; import { ValidationError } from 'universe/backend/error'; import { ObjectId } from 'mongodb'; import type { NextApiResponse, NextApiRequest } from 'next'; +import type { FriendRequestType, UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -12,14 +17,15 @@ export { defaultConfig as config } from 'universe/backend/middleware'; export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { - let followed_id: ObjectId | undefined = undefined; - let user_id: ObjectId | undefined = undefined; + let target_id: UserId | undefined = undefined; + let user_id: UserId | undefined = undefined; + let request_type: FriendRequestType | undefined = undefined; try { - followed_id = new ObjectId(req.query.followed_id.toString()); + target_id = new ObjectId(req.query.target_id.toString()); } catch { throw new ValidationError( - `invalid user_id "${req.query.followed_id.toString()}"` + `invalid target_id "${req.query.target_id.toString()}"` ); } @@ -29,15 +35,24 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); } + if (!['incoming', 'outgoing'].includes(req.query.request_type.toString())) { + throw new ValidationError( + `invalid request_type "${req.query.request_type.toString()}", expected "incoming" or "outgoing"` + ); + } + + request_type = req.query.request_type as FriendRequestType; + if (req.method == 'GET') { - (await isUserFollowing({ followed_id, user_id })) + (await isFriendRequestOfType({ target_id, request_type, user_id })) ? sendHttpOk(res) : sendHttpNotFound(res); } else if (req.method == 'DELETE') { - await unfollowUser({ followed_id, user_id }); + await removeFriendRequest({ target_id, request_type, user_id }); sendHttpOk(res); } else { - await addFriendRequest({ followed_id, user_id }); + // * PUT + await addFriendRequest({ target_id, request_type, user_id }); sendHttpOk(res); } }, diff --git a/src/pages/api/v1/users/[user_id]/requests/[request_type]/index.ts b/src/pages/api/v1/users/[user_id]/requests/[request_type]/index.ts index 3da70c8..09e7fbd 100644 --- a/src/pages/api/v1/users/[user_id]/requests/[request_type]/index.ts +++ b/src/pages/api/v1/users/[user_id]/requests/[request_type]/index.ts @@ -1,10 +1,11 @@ -import { getUserFriendsUserIds } from 'universe/backend'; +import { getFriendRequestsOfType } from 'universe/backend'; import { sendHttpOk } from 'multiverse/next-respond'; import { wrapHandler } from 'universe/backend/middleware'; import { ValidationError } from 'universe/backend/error'; import { ObjectId } from 'mongodb'; import type { NextApiResponse, NextApiRequest } from 'next'; +import type { FriendRequestType, UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -12,9 +13,9 @@ export { defaultConfig as config } from 'universe/backend/middleware'; export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { - let after: ObjectId | null | undefined = undefined; - let user_id: ObjectId | undefined = undefined; - const includeIndirect = req.query.includeIndirect !== undefined; + let after: UserId | null | undefined = undefined; + let user_id: UserId | undefined = undefined; + let request_type: FriendRequestType | undefined = undefined; try { after = req.query.after ? new ObjectId(req.query.after.toString()) : null; @@ -28,8 +29,17 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { throw new ValidationError(`invalid user_id "${req.query.user_id.toString()}"`); } + if (!['incoming', 'outgoing'].includes(req.query.request_type.toString())) { + throw new ValidationError( + `invalid request_type "${req.query.request_type.toString()}", expected "incoming" or "outgoing"` + ); + } + + request_type = req.query.request_type as FriendRequestType; + + // * GET sendHttpOk(res, { - users: await getUserFriendsUserIds({ user_id, includeIndirect, after }) + users: await getFriendRequestsOfType({ user_id, request_type, after }) }); }, { diff --git a/src/pages/api/v1/users/index.ts b/src/pages/api/v1/users/index.ts index e527592..b3cd7be 100644 --- a/src/pages/api/v1/users/index.ts +++ b/src/pages/api/v1/users/index.ts @@ -5,6 +5,7 @@ import { ValidationError } from 'universe/backend/error'; import { ObjectId } from 'mongodb'; import type { NextApiResponse, NextApiRequest } from 'next'; +import type { UserId } from 'types/global'; // ? This is a NextJS special "config" export export { defaultConfig as config } from 'universe/backend/middleware'; @@ -13,7 +14,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { await wrapHandler( async ({ req, res }) => { const key = req.headers.key?.toString() || ''; - let after: ObjectId | null | undefined = undefined; + let after: UserId | null | undefined = undefined; try { after = req.query.after ? new ObjectId(req.query.after.toString()) : null; @@ -23,7 +24,9 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { if (req.method == 'GET') { sendHttpOk(res, { users: await getAllUsers({ after }) }); - } else sendHttpOk(res, { user: await createUser({ key, data: req.body }) }); + // * POST + } else + sendHttpOk(res, { user: await createUser({ creatorKey: key, data: req.body }) }); }, { req, res, methods: ['GET', 'POST'], apiVersion: 1 } ); diff --git a/test/integration.test.ts b/test/integration.test.ts index 6d9612e..80303aa 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -10,6 +10,9 @@ import EndpointBarks, { config as ConfigBarks } from 'universe/pages/api/v1/bark import EndpointUsers, { config as ConfigUsers } from 'universe/pages/api/v1/users'; import EndpointInfo, { config as ConfigInfo } from 'universe/pages/api/v1/info'; +// TODO: make this into a generalized package of some sort... call it: +// TODO: @xunnamius/fable + import EndpointBarksIds, { config as ConfigBarksIds } from 'universe/pages/api/v1/barks/[...meme_ids]'; diff --git a/test/unit-api-memes.test.ts b/test/unit-api-memes.test.ts index da19a51..c463d7e 100644 --- a/test/unit-api-memes.test.ts +++ b/test/unit-api-memes.test.ts @@ -13,7 +13,6 @@ import { DUMMY_KEY as KEY, createMeme, getMemes, - updateMemes, getMemeLikesUserIds, isMemeLiked, searchMemes @@ -43,7 +42,6 @@ jest.mock('universe/backend/middleware'); const mockedCreateMeme = asMockedFunction(createMeme); const mockedGetMemes = asMockedFunction(getMemes); -const mockedUpdateMemes = asMockedFunction(updateMemes); const mockedGetMemeLikesUserIds = asMockedFunction(getMemeLikesUserIds); const mockedIsMemeLiked = asMockedFunction(isMemeLiked); const mockedSearchMemes = asMockedFunction(searchMemes); @@ -228,7 +226,7 @@ describe('api/v1/memes', () => { }); describe('/:meme_id1/:meme_id2/.../:meme_idN [PUT]', () => { - it('accepts multiple meme_ids, ignoring not found and duplicates', async () => { + it('accepts multiple meme_ids', async () => { expect.hasAssertions(); const items = [ @@ -388,7 +386,7 @@ describe('api/v1/memes', () => { }); }); - it('errors if the user has not liked the meme', async () => { + it('404s if the user has not liked the meme', async () => { expect.hasAssertions(); mockedIsMemeLiked.mockReturnValue(Promise.resolve(false)); diff --git a/test/unit-api-users.test.ts b/test/unit-api-users.test.ts index d2be7f2..66125e7 100644 --- a/test/unit-api-users.test.ts +++ b/test/unit-api-users.test.ts @@ -14,21 +14,13 @@ import { getAllUsers, createUser, getUser, - deleteUser, - updateUser, getMemeLikesUserIds, getUserLikedMemeIds, isMemeLiked, - removeLikedMeme, - addLikedMeme, getUserFriendsUserIds, isUserAFriend, - removeUserAsFriend, - addUserAsFriend, getFriendRequestsOfType, - isFriendRequestOfType, - removeFriendRequest, - addFriendRequest + isFriendRequestOfType } from 'universe/backend'; import EndpointUsers, { config as ConfigUsers } from 'universe/pages/api/v1/users'; @@ -61,7 +53,7 @@ import EndpointUsersIdRequestsTypeId, { config as ConfigUsersIdRequestsTypeId } from 'universe/pages/api/v1/users/[user_id]/requests/[request_type]/[target_id]'; -import type { PublicUser } from 'types/global'; +import type { FriendRequestType, PublicUser } from 'types/global'; jest.mock('universe/backend'); jest.mock('universe/backend/middleware'); @@ -69,21 +61,13 @@ jest.mock('universe/backend/middleware'); const mockedGetAllUsers = asMockedFunction(getAllUsers); const mockedCreateUser = asMockedFunction(createUser); const mockedGetUser = asMockedFunction(getUser); -const mockedDeleteUser = asMockedFunction(deleteUser); -const mockedUpdateUser = asMockedFunction(updateUser); const mockedGetMemeLikesUserIds = asMockedFunction(getMemeLikesUserIds); const mockedGetUserLikedMemeIds = asMockedFunction(getUserLikedMemeIds); const mockedIsMemeLiked = asMockedFunction(isMemeLiked); -const mockedRemoveLikedMeme = asMockedFunction(removeLikedMeme); -const mockedAddLikedMeme = asMockedFunction(addLikedMeme); const mockedGetUserFriendsUserIds = asMockedFunction(getUserFriendsUserIds); const mockedIsUserAFriend = asMockedFunction(isUserAFriend); -const mockedRemoveUserAsFriend = asMockedFunction(removeUserAsFriend); -const mockedAddUserAsFriend = asMockedFunction(addUserAsFriend); const mockedGetFriendRequestsOfType = asMockedFunction(getFriendRequestsOfType); const mockedIsFriendRequestOfType = asMockedFunction(isFriendRequestOfType); -const mockedRemoveFriendRequest = asMockedFunction(removeFriendRequest); -const mockedAddFriendRequest = asMockedFunction(addFriendRequest); const api = { users: EndpointUsers as typeof EndpointUsers & { config?: typeof ConfigUsers }, @@ -232,13 +216,14 @@ describe('api/v1/users', () => { const json = await fetch({ headers: { KEY } }).then((r) => r.json()); + expect(mockedGetUser).toBeCalledWith({ user_id: expect.anything() }); expect(json.success).toBeTrue(); expect(json.user).toBeObject(); } }); await testApiHandler({ - params: { user_id: 'invalid' }, + params: { user_id: '' }, handler: api.usersId, test: async ({ fetch }) => expect(await fetch().then((r) => r.status)).toStrictEqual(400) @@ -246,6 +231,28 @@ describe('api/v1/users', () => { }); }); + describe('/:username [GET]', () => { + it('accepts a username and returns a user; errors on empty username/missing params', async () => { + expect.hasAssertions(); + + await testApiHandler({ + params: { user_id: 'faker' }, + handler: api.usersId, + test: async ({ fetch }) => { + mockedGetUser.mockReturnValue( + Promise.resolve({}) as ReturnType + ); + + const json = await fetch({ headers: { KEY } }).then((r) => r.json()); + + expect(mockedGetUser).toBeCalledWith({ username: expect.anything() }); + expect(json.success).toBeTrue(); + expect(json.user).toBeObject(); + } + }); + }); + }); + describe('/:user_id [DELETE]', () => { it('accepts a user_id; errors on invalid user_id', async () => { expect.hasAssertions(); @@ -433,7 +440,7 @@ describe('api/v1/users', () => { }); }); - it('errors if the user has not liked the meme', async () => { + it('404s if the user has not liked the meme', async () => { expect.hasAssertions(); mockedIsMemeLiked.mockReturnValue(Promise.resolve(false)); @@ -453,7 +460,7 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/following [GET]', () => { + describe('/:user_id/friends [GET]', () => { it('accepts user_id and returns users; errors if invalid user_id given', async () => { expect.hasAssertions(); @@ -485,7 +492,7 @@ describe('api/v1/users', () => { }); }); - it('supports pagination and includeIndirect flag', async () => { + it('supports pagination', async () => { expect.hasAssertions(); await testApiHandler({ @@ -497,24 +504,6 @@ describe('api/v1/users', () => { expect(json.success).toBeTrue(); expect(json.users).toBeArray(); - expect(mockedGetFollowingUserIds).toBeCalledWith( - expect.objectContaining({ includeIndirect: false }) - ); - } - }); - - await testApiHandler({ - params: { user_id: new ObjectId().toString() }, - requestPatcher: (req) => (req.url = '/?includeIndirect'), - handler: api.usersIdFriends, - test: async ({ fetch }) => { - const json = await fetch({ headers: { KEY } }).then((r) => r.json()); - - expect(json.success).toBeTrue(); - expect(json.users).toBeArray(); - expect(mockedGetFollowingUserIds).toBeCalledWith( - expect.objectContaining({ includeIndirect: true }) - ); } }); }); @@ -552,26 +541,26 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/following/:followed_id [GET]', () => { - it('accepts followed_id and user_id; errors if invalid IDs given', async () => { + describe('/:user_id/friends/:friend_id [GET]', () => { + it('accepts friend_id and user_id; errors if invalid IDs given', async () => { expect.hasAssertions(); - mockedIsUserFollowing.mockReturnValue(Promise.resolve(true)); + mockedIsUserAFriend.mockReturnValue(Promise.resolve(true)); const factory = itemFactory([ - [{ followed_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ followed_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ followed_id: 'invalid-id', user_id: 'invalid-id' }, 400], + [{ friend_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], + [{ friend_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], + [{ friend_id: 'invalid-id', user_id: 'invalid-id' }, 400], [ { - followed_id: new ObjectId().toString(), + friend_id: new ObjectId().toString(), user_id: new ObjectId().toString() }, 200 ] ]); - const params = { followed_id: '', user_id: '' }; + const params = { friend_id: '', user_id: '' }; await testApiHandler({ params, @@ -585,15 +574,15 @@ describe('api/v1/users', () => { }); }); - it('errors if followed_id is not actually followed', async () => { + it('404s if the users are not friends', async () => { expect.hasAssertions(); - mockedIsUserFollowing.mockReturnValue(Promise.resolve(false)); + mockedIsUserAFriend.mockReturnValue(Promise.resolve(false)); await testApiHandler({ params: { - followed_id: new ObjectId().toString(), - user_id: new ObjectId().toString() + user_id: new ObjectId().toString(), + friend_id: new ObjectId().toString() }, handler: api.usersIdFriendsId, test: async ({ fetch }) => { @@ -605,24 +594,24 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/following/:followed_id [DELETE]', () => { - it('accepts followed_id and user_id; errors if invalid IDs given', async () => { + describe('/:user_id/friends/:friend_id [DELETE]', () => { + it('accepts friend_id and user_id; errors if invalid IDs given', async () => { expect.hasAssertions(); const factory = itemFactory([ - [{ followed_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ followed_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ followed_id: 'invalid-id', user_id: 'invalid-id' }, 400], + [{ friend_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], + [{ friend_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], + [{ friend_id: 'invalid-id', user_id: 'invalid-id' }, 400], [ { - followed_id: new ObjectId().toString(), + friend_id: new ObjectId().toString(), user_id: new ObjectId().toString() }, 200 ] ]); - const params = { followed_id: '', user_id: '' }; + const params = { friend_id: '', user_id: '' }; await testApiHandler({ params, @@ -639,24 +628,24 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/following/:followed_id [PUT]', () => { - it('accepts followed_id and user_id; errors if invalid IDs given', async () => { + describe('/:user_id/friends/:friend_id [PUT]', () => { + it('accepts friend_id and user_id; errors if invalid IDs given', async () => { expect.hasAssertions(); const factory = itemFactory([ - [{ followed_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ followed_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ followed_id: 'invalid-id', user_id: 'invalid-id' }, 400], + [{ friend_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], + [{ friend_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], + [{ friend_id: 'invalid-id', user_id: 'invalid-id' }, 400], [ { - followed_id: new ObjectId().toString(), + friend_id: new ObjectId().toString(), user_id: new ObjectId().toString() }, 200 ] ]); - const params = { followed_id: '', user_id: '' }; + const params = { friend_id: '', user_id: '' }; await testApiHandler({ params, @@ -673,16 +662,16 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/pack [GET]', () => { - it('accepts user_id and returns users; errors if invalid user_id given', async () => { + describe('/:user_id/requests/:request_type [GET]', () => { + it('accepts user_id and request_type and returns users', async () => { expect.hasAssertions(); const factory = itemFactory([ - [{ user_id: 'invalid-id' }, 400], - [{ user_id: new ObjectId().toString() }, 200] + [{ user_id: new ObjectId().toString(), request_type: 'outgoing' }, 200], + [{ user_id: new ObjectId().toString(), request_type: 'incoming' }, 200] ]); - const params = { user_id: '' }; + const params = {} as { user_id: string; request_type: FriendRequestType }; await testApiHandler({ params, @@ -705,190 +694,19 @@ describe('api/v1/users', () => { }); }); - it('supports pagination', async () => { - expect.hasAssertions(); - - await testApiHandler({ - params: { user_id: new ObjectId().toString() }, - requestPatcher: (req) => (req.url = `/?after=${new ObjectId()}`), - handler: api.usersIdRequestsType, - test: async ({ fetch }) => { - const json = await fetch({ headers: { KEY } }).then((r) => r.json()); - - expect(json.success).toBeTrue(); - expect(json.users).toBeArray(); - } - }); - }); - - it('handles invalid offsets during pagination', async () => { - expect.hasAssertions(); - - const factory = itemFactory([ - `/?after=-5`, - `/?after=a`, - `/?after=@($)`, - `/?after=xyz`, - `/?after=123`, - `/?after=(*$)`, - `/?dne=123` - ]); - - await testApiHandler({ - requestPatcher: (req) => (req.url = factory()), - params: { user_id: new ObjectId().toString() }, - handler: api.usersIdRequestsType, - test: async ({ fetch }) => { - const responses = await Promise.all( - Array.from({ length: factory.count }).map((_) => { - return fetch({ headers: { KEY } }).then((r) => r.status); - }) - ); - - expect(responses).toIncludeSameMembers([ - ...Array.from({ length: factory.count - 1 }).map(() => 400), - 200 - ]); - } - }); - }); - }); - - describe('/:user_id/pack/:packmate_id [GET]', () => { - it('accepts packmate_id and user_id; errors if invalid IDs given', async () => { + it('errors if invalid user_id or request_type given', async () => { expect.hasAssertions(); - mockedIsUserPackmate.mockReturnValue(Promise.resolve(true)); - const factory = itemFactory([ - [{ packmate_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ packmate_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ packmate_id: 'invalid-id', user_id: 'invalid-id' }, 400], - [ - { - packmate_id: new ObjectId().toString(), - user_id: new ObjectId().toString() - }, - 200 - ] + [{ user_id: 'invalid-id', request_type: 'incoming' }, 400], + [{ user_id: new ObjectId().toString(), request_type: 'bad' }, 400] ]); - const params = { packmate_id: '', user_id: '' }; + const params = {} as { user_id: string; request_type: FriendRequestType }; await testApiHandler({ params, - handler: api.usersIdRequestsTypeId, - test: async ({ fetch }) => { - for (const [expectedParams, expectedStatus] of factory) { - Object.assign(params, expectedParams); - expect(await fetch().then((r) => r.status)).toStrictEqual(expectedStatus); - } - } - }); - }); - - it('errors if packmate_id does not belong to a packmate', async () => { - expect.hasAssertions(); - - mockedIsUserPackmate.mockReturnValue(Promise.resolve(false)); - - await testApiHandler({ - params: { - packmate_id: new ObjectId().toString(), - user_id: new ObjectId().toString() - }, - handler: api.usersIdRequestsTypeId, - test: async ({ fetch }) => { - expect(await fetch({ headers: { KEY } }).then((r) => r.status)).toStrictEqual( - 404 - ); - } - }); - }); - }); - - describe('/:user_id/pack/:packmate_id [DELETE]', () => { - it('accepts packmate_id and user_id; errors if invalid IDs given', async () => { - expect.hasAssertions(); - - const factory = itemFactory([ - [{ packmate_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ packmate_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ packmate_id: 'invalid-id', user_id: 'invalid-id' }, 400], - [ - { - packmate_id: new ObjectId().toString(), - user_id: new ObjectId().toString() - }, - 200 - ] - ]); - - const params = { packmate_id: '', user_id: '' }; - - await testApiHandler({ - params, - handler: api.usersIdRequestsTypeId, - test: async ({ fetch }) => { - for (const [expectedParams, expectedStatus] of factory) { - Object.assign(params, expectedParams); - expect( - await fetch({ method: 'DELETE', headers: { KEY } }).then((r) => r.status) - ).toStrictEqual(expectedStatus); - } - } - }); - }); - }); - - describe('/:user_id/pack/:packmate_id [PUT]', () => { - it('accepts packmate_id and user_id; errors if invalid IDs given', async () => { - expect.hasAssertions(); - - const factory = itemFactory([ - [{ packmate_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ packmate_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ packmate_id: 'invalid-id', user_id: 'invalid-id' }, 400], - [ - { - packmate_id: new ObjectId().toString(), - user_id: new ObjectId().toString() - }, - 200 - ] - ]); - - const params = { packmate_id: '', user_id: '' }; - - await testApiHandler({ - params, - handler: api.usersIdRequestsTypeId, - test: async ({ fetch }) => { - for (const [expectedParams, expectedStatus] of factory) { - Object.assign(params, expectedParams); - expect( - await fetch({ method: 'PUT', headers: { KEY } }).then((r) => r.status) - ).toStrictEqual(expectedStatus); - } - } - }); - }); - }); - - describe('/:user_id/bookmarks [GET]', () => { - it('accepts user_id and returns memes; errors if invalid user_id given', async () => { - expect.hasAssertions(); - - const factory = itemFactory([ - [{ user_id: 'invalid-id' }, 400], - [{ user_id: new ObjectId().toString() }, 200] - ]); - - const params = { user_id: '' }; - - await testApiHandler({ - params, - handler: api.usersIdBookmarks, + handler: api.usersIdRequestsType, test: async ({ fetch }) => { for (const [expectedParams, expectedStatus] of factory) { Object.assign(params, expectedParams); @@ -899,7 +717,7 @@ describe('api/v1/users', () => { ).toStrictEqual([ expectedStatus, expectedStatus == 200 - ? { success: true, memes: expect.any(Array) } + ? { success: true, users: expect.any(Array) } : expect.objectContaining({ success: false }) ]); } @@ -911,14 +729,14 @@ describe('api/v1/users', () => { expect.hasAssertions(); await testApiHandler({ - params: { user_id: new ObjectId().toString() }, + params: { user_id: new ObjectId().toString(), request_type: 'incoming' }, requestPatcher: (req) => (req.url = `/?after=${new ObjectId()}`), - handler: api.usersIdBookmarks, + handler: api.usersIdRequestsType, test: async ({ fetch }) => { const json = await fetch({ headers: { KEY } }).then((r) => r.json()); expect(json.success).toBeTrue(); - expect(json.memes).toBeArray(); + expect(json.users).toBeArray(); } }); }); @@ -938,8 +756,8 @@ describe('api/v1/users', () => { await testApiHandler({ requestPatcher: (req) => (req.url = factory()), - params: { user_id: new ObjectId().toString() }, - handler: api.usersIdBookmarks, + params: { user_id: new ObjectId().toString(), request_type: 'incoming' }, + handler: api.usersIdRequestsType, test: async ({ fetch }) => { const responses = await Promise.all( Array.from({ length: factory.count }).map((_) => { @@ -956,30 +774,56 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/bookmarks/:meme_id [GET]', () => { - it('accepts meme_id and user_id; errors if invalid IDs given', async () => { + describe('/:user_id/requests/:request_type/:target_id [GET]', () => { + it('accepts target_id, user_id, and request_type; errors if invalid IDs given', async () => { expect.hasAssertions(); - mockedIsMemeBookmarked.mockReturnValue(Promise.resolve(true)); + mockedIsFriendRequestOfType.mockReturnValue(Promise.resolve(true)); const factory = itemFactory([ - [{ meme_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ meme_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ meme_id: 'invalid-id', user_id: 'invalid-id' }, 400], [ { - meme_id: new ObjectId().toString(), - user_id: new ObjectId().toString() + target_id: 'invalid-id', + user_id: new ObjectId().toString(), + request_type: 'incoming' + }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: 'invalid-id', + request_type: 'incoming' + }, + 400 + ], + [ + { target_id: 'invalid-id', user_id: 'invalid-id', request_type: 'incoming' }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: new ObjectId().toString(), + request_type: 'invalid-type' + }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: new ObjectId().toString(), + request_type: 'incoming' }, 200 ] ]); - const params = { meme_id: '', user_id: '' }; + const params = { target_id: '', user_id: '' }; await testApiHandler({ params, - handler: api.usersIdBookmarksId, + handler: api.usersIdRequestsTypeId, test: async ({ fetch }) => { for (const [expectedParams, expectedStatus] of factory) { Object.assign(params, expectedParams); @@ -989,17 +833,18 @@ describe('api/v1/users', () => { }); }); - it('errors if meme_id does not belong to a packmate', async () => { + it('404s if the friend request does not exist', async () => { expect.hasAssertions(); - mockedIsMemeBookmarked.mockReturnValue(Promise.resolve(false)); + mockedIsFriendRequestOfType.mockReturnValue(Promise.resolve(false)); await testApiHandler({ params: { - meme_id: new ObjectId().toString(), - user_id: new ObjectId().toString() + user_id: new ObjectId().toString(), + target_id: new ObjectId().toString(), + request_type: 'incoming' }, - handler: api.usersIdBookmarksId, + handler: api.usersIdRequestsTypeId, test: async ({ fetch }) => { expect(await fetch({ headers: { KEY } }).then((r) => r.status)).toStrictEqual( 404 @@ -1009,28 +854,54 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/bookmarks/:meme_id [DELETE]', () => { - it('accepts meme_id and user_id; errors if invalid IDs given', async () => { + describe('/:user_id/requests/:request_type/:target_id [DELETE]', () => { + it('accepts target_id, user_id, and request_type; errors if invalid IDs given', async () => { expect.hasAssertions(); const factory = itemFactory([ - [{ meme_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ meme_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ meme_id: 'invalid-id', user_id: 'invalid-id' }, 400], [ { - meme_id: new ObjectId().toString(), - user_id: new ObjectId().toString() + target_id: 'invalid-id', + user_id: new ObjectId().toString(), + request_type: 'outgoing' + }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: 'invalid-id', + request_type: 'outgoing' + }, + 400 + ], + [ + { target_id: 'invalid-id', user_id: 'invalid-id', request_type: 'outgoing' }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: new ObjectId().toString(), + request_type: 'faker' + }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: new ObjectId().toString(), + request_type: 'outgoing' }, 200 ] ]); - const params = { meme_id: '', user_id: '' }; + const params = { target_id: '', user_id: '' }; await testApiHandler({ params, - handler: api.usersIdBookmarksId, + handler: api.usersIdRequestsTypeId, test: async ({ fetch }) => { for (const [expectedParams, expectedStatus] of factory) { Object.assign(params, expectedParams); @@ -1043,28 +914,54 @@ describe('api/v1/users', () => { }); }); - describe('/:user_id/bookmarks/:meme_id [PUT]', () => { - it('accepts meme_id and user_id; errors if invalid IDs given', async () => { + describe('/:user_id/requests/:request_type/:target_id [PUT]', () => { + it('accepts target_id, user_id, and request_type; errors if invalid IDs given', async () => { expect.hasAssertions(); const factory = itemFactory([ - [{ meme_id: 'invalid-id', user_id: new ObjectId().toString() }, 400], - [{ meme_id: new ObjectId().toString(), user_id: 'invalid-id' }, 400], - [{ meme_id: 'invalid-id', user_id: 'invalid-id' }, 400], [ { - meme_id: new ObjectId().toString(), - user_id: new ObjectId().toString() + target_id: 'invalid-id', + user_id: new ObjectId().toString(), + request_type: 'outgoing' + }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: 'invalid-id', + request_type: 'outgoing' + }, + 400 + ], + [ + { target_id: 'invalid-id', user_id: 'invalid-id', request_type: 'outgoing' }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: new ObjectId().toString(), + request_type: 'bad' + }, + 400 + ], + [ + { + target_id: new ObjectId().toString(), + user_id: new ObjectId().toString(), + request_type: 'outgoing' }, 200 ] ]); - const params = { meme_id: '', user_id: '' }; + const params = { target_id: '', user_id: '' }; await testApiHandler({ params, - handler: api.usersIdBookmarksId, + handler: api.usersIdRequestsTypeId, test: async ({ fetch }) => { for (const [expectedParams, expectedStatus] of factory) { Object.assign(params, expectedParams);