Skip to content

Commit

Permalink
feat: Add hook to handle invite code (#625)
Browse files Browse the repository at this point in the history
* linting

* add hook to handle invite code

* add the files...

* linting

* fix tests

* fix tests

* fix tests

* undo accidental stash pop

* linting

* ignore lint rule to fix over triggering of useEffect
  • Loading branch information
alexanderallen-aa authored Jan 3, 2024
1 parent 0c2f16f commit 294bf26
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 36 deletions.
6 changes: 4 additions & 2 deletions packages/api/src/api/project/contributors/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export const put = async (req: Request, res: Response) => {
});
if (errorHandled) return;

const project = await entityManager.findOneOrFail(Project, { id: data.projectId });
if (project.inviteCode !== data.inviteCode) {
let project: Project;
try {
project = await entityManager.findOneOrFail(Project, { inviteCode: data.inviteCode });
} catch {
res.sendStatus(404);
return;
}
Expand Down
37 changes: 13 additions & 24 deletions packages/api/tests/api/project/contributors/put.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { validatePayload } from '../../../../src/utils/validatePayload';

jest.mock('../../../../src/utils/validatePayload');
const validatePayloadMock = getMock(validatePayload);
const validPayload = { projectId: '1', inviteCode: '00000000-0000-0000-0000-000000000000' };
const validPayload = { inviteCode: '00000000-0000-0000-0000-000000000000' };
const mockUser = { id: '1' };

describe('project contributors put endpoint', () => {
Expand All @@ -27,21 +27,6 @@ describe('project contributors put endpoint', () => {
expect(res.send).toHaveBeenCalledWith(mockProject);
});

it('should return 404 if the invite code is invalid', async () => {
validatePayloadMock.mockReturnValueOnce({ errorHandled: false, data: validPayload } as any);
const req = createMockRequest({ user: mockUser, ...validPayload } as any);
const res = createMockResponse();
const mockProject = { inviteCode: 'invalid', contributors: { add: jest.fn() } };
req.entityManager.findOneOrFail.mockResolvedValueOnce(mockProject);

await put(req as any, res as any);

expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId });
expect(mockProject.contributors.add).not.toBeCalled();
expect(req.entityManager.persist).not.toBeCalled();
expect(res.sendStatus).toHaveBeenCalledWith(404);
});

it('should return 409 if the user is already a contributor', async () => {
validatePayloadMock.mockReturnValueOnce({ errorHandled: false, data: validPayload } as any);
const req = createMockRequest({ user: mockUser, ...validPayload } as any);
Expand All @@ -52,7 +37,9 @@ describe('project contributors put endpoint', () => {

await put(req as any, res as any);

expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId });
expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, {
inviteCode: validPayload.inviteCode,
});
expect(mockProject.contributors.add).not.toBeCalled();
expect(req.entityManager.persist).not.toBeCalled();
expect(res.sendStatus).toHaveBeenCalledWith(409);
Expand All @@ -68,7 +55,9 @@ describe('project contributors put endpoint', () => {

await put(req as any, res as any);

expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId });
expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, {
inviteCode: validPayload.inviteCode,
});
expect(mockProject.contributors.add).not.toBeCalled();
expect(req.entityManager.persist).not.toBeCalled();
expect(res.sendStatus).toHaveBeenCalledWith(423);
Expand All @@ -84,7 +73,9 @@ describe('project contributors put endpoint', () => {

await put(req as any, res as any);

expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId });
expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, {
inviteCode: validPayload.inviteCode,
});
expect(mockProject.contributors.add).not.toBeCalled();
expect(req.entityManager.persist).not.toBeCalled();
expect(res.sendStatus).toHaveBeenCalledWith(500);
Expand All @@ -105,15 +96,13 @@ describe('project contributors put endpoint', () => {
validatePayloadMock.mockReturnValueOnce({ errorHandled: false, data: validPayload } as any);
const req = createMockRequest({ user: mockUser, ...validPayload } as any);
const res = createMockResponse();
req.entityManager.findOneOrFail.mockResolvedValueOnce({
...validPayload,
inviteCode: '2',
} as any);
req.entityManager.findOneOrFail.mockRejectedValueOnce({ name: 'NotFoundError' });

await put(req as any, res as any);

expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId });
expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, {
inviteCode: validPayload.inviteCode,
});
expect(res.sendStatus).toHaveBeenCalledWith(404);
});
});
1 change: 0 additions & 1 deletion packages/shared/src/schema/project/contributors/put.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { z } from 'zod';

export const put = z.object({
projectId: z.string(),
inviteCode: z.string().uuid(),
});
9 changes: 0 additions & 9 deletions packages/shared/tests/schema/project/contributors/put.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ describe('project contributors put schema', () => {
expect(Schema.project.contributors.put.safeParse(validPut).success).toBe(true);
});

it('does not validate an object with an invalid projectId', () => {
expect(
Schema.project.contributors.put.safeParse({
...validPut,
projectId: 1,
}).success,
).toBe(false);
});

it('does not validate an object with an invalid inviteCode', () => {
expect(
Schema.project.contributors.put.safeParse({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useProjectInviteCodeHandler';
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';
import { useUserStore } from '../../stores/user';
import { openErrorToast, openSuccessToast } from '../utils/CustomToast';
import { useRedirectToAuth } from '../layout/RedirectToAuthModal';

const toasts = {
Success: () =>
openSuccessToast({
title: `Registration successful!`,
description: "You're all set! 🚀",
}),
Invalid: () =>
openErrorToast({
title: 'Invalid invite code',
description: 'Please check your invite code and try again.',
}),
ProjectExists: () =>
openErrorToast({
title: 'Project already exists',
description: 'You already have a project.',
}),
};

const useProjectInviteCodeHandler = () => {
const router = useRouter();
const { triggerRedirect } = useRedirectToAuth();
const { user, doneLoading: userLoaded } = useUserStore();
const { projectInviteCode } = router.query;

useEffect(() => {
if (!userLoaded || !projectInviteCode) return;
if (!user) {
triggerRedirect({ returnTo: `/?projectInviteCode=${projectInviteCode}` });
return;
}
if (user?.project) {
toasts.ProjectExists();
return;
}

void (async () => {
let project;
try {
project = (await axios.put(`/api/project/contributors`, { inviteCode: projectInviteCode }))
.data;
} catch {
toasts.Invalid();
return;
}
toasts.Success();

await useUserStore.getState().fetchUser();
void router.push(`/project/${project.id}`);
})();
}, [projectInviteCode, userLoaded, triggerRedirect]); // eslint-disable-line react-hooks/exhaustive-deps
};

export { useProjectInviteCodeHandler };
2 changes: 2 additions & 0 deletions packages/web/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { PageContainer } from '../components/layout/PageContainer';
import { Prizes } from '../components/Prizes';
import { usePrizesStore } from '../stores/prizes';
import { ProjectRegistrationCTA } from '../components/ProjectRegistrationCTA';
import { useProjectInviteCodeHandler } from '../components/useProjectInviteCodeHandler';

const Home: NextPage = () => {
useProjectInviteCodeHandler();
const { doneLoading: prizesFetched } = usePrizesStore();
React.useEffect(() => {
const { prizes } = usePrizesStore.getState();
Expand Down

0 comments on commit 294bf26

Please sign in to comment.