Skip to content

Commit

Permalink
feat: implement repo and create tests (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
Clement-Muth authored Oct 25, 2024
1 parent fe509a8 commit 8fd5315
Show file tree
Hide file tree
Showing 39 changed files with 579 additions and 102 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 9 additions & 0 deletions __mocks__/gettextFileTransformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable no-undef */

module.exports = {
process() {
return {
code: "module.exports = { messages: {} }"
};
}
};
16 changes: 8 additions & 8 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
const nextJest = require("next/jest");
import type { Config } from 'jest'
import nextJest from "next/jest";

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./"
});

// Add any custom config to be passed to Jest
const customJestConfig = {
const customJestConfig: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ["<rootDir>/src/test/setupTests.ts"],
setupFiles: ["jest-canvas-mock"],
testSequencer: "<rootDir>/jestTestSequencer.js",
// testSequencer: "<rootDir>/jestTestSequencer.ts",
resetMocks: true,
modulePaths: ["<rootDir>/src"],
moduleNameMapper: {
Expand All @@ -20,8 +23,6 @@ const customJestConfig = {
"^@lingui\\/loader!(.+.po)$": "<rootDir>/src/translations/$1",
"^~/(.*)$": "<rootDir>/src/$1"
},
collectCoverage: false,
testEnvironment: "jsdom",
transformIgnorePatterns: ["node_modules/(?!(ky|@react-hook/throttle|@react-hook/latest)/)"],
transform: {
"^.+\\.po$": "<rootDir>/__mocks__/gettextFileTransformer.js",
Expand All @@ -34,11 +35,10 @@ const customJestConfig = {
runtime: "automatic"
}
}
},
}
}
]
},
extensionsToTreatAsEsm: ['.tsx']
}
};

// createJestConfig is exported in this way to ensure that next/jest can load the Next.js configuration, which is async
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"typescript:check": "tsc --noEmit",
"translation:extract": "lingui extract --clean",
"compile": "lingui compile --typescript",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules env-cmd -f ./.env.test jest --maxWorkers=100%",
"test": "env-cmd -f ./.env.test jest --maxWorkers=100%",
"generate": "kysely-codegen --out-file ./src/types/db.d.ts",
"prepare": "husky",
"postinstall": "prisma generate"
Expand Down Expand Up @@ -84,11 +84,13 @@
"jest": "^29.5.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.5.0",
"jest-mock-extended": "^4.0.0-beta1",
"kysely-codegen": "^0.17.0",
"lint-staged": "^15.2.10",
"prisma": "^5.21.1",
"semantic-release": "^24.1.0",
"sharp": "^0.33.5",
"ts-node": "^10.9.2",
"turbo": "^2.2.2",
"webpack": "^5.95.0",
"zod": "^3.23.8"
Expand Down
7 changes: 4 additions & 3 deletions src/app/api/user/[id]/account/delete/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { del, list } from "@vercel/blob";
import { withAuthentication } from "~/libraries/nextauth/authConfig";
import { prisma } from "~/libraries/prisma";

export const DELETE = async (request: Request, { params }: { params: Promise<{ id: string }> }) => {
const { id } = await params;
export const DELETE = withAuthentication(async (request, ctx) => {
const { id } = await (ctx.params as unknown as Promise<{ id: string }>);

await prisma.user.delete({ where: { id } });

Expand All @@ -11,4 +12,4 @@ export const DELETE = async (request: Request, { params }: { params: Promise<{ i
if (userAvatars.blobs.length > 0) await del(userAvatars.blobs.map((blob) => blob.url));

return new Response(null, { status: 204 });
};
});
34 changes: 19 additions & 15 deletions src/app/api/user/[id]/profile/route.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
import { put } from "@vercel/blob";
import { NextResponse } from "next/server";
import { z } from "zod";
import { PrismaProfileRepository } from "~/applications/Profile/Infrastructure/PrismaUserRepository";
import { withAuthentication } from "~/libraries/nextauth/authConfig";
import { prisma } from "~/libraries/prisma";

const RequestSchema = z.object({
phone: z.string().optional(),
name: z.string().optional()
});

const profileRepository = new PrismaProfileRepository();

/**
* Updates a user profile.
*
* Returns: Updated user
*/
export const PATCH = async (request: Request, { params }: { params: Promise<{ id: string }> }) => {
const { id } = await params;
export const PATCH = withAuthentication(async (request, ctx): Promise<NextResponse> => {
const { id } = await (ctx.params as unknown as Promise<{ id: string }>);

const payload = RequestSchema.parse(await request.json());
try {
const payload = RequestSchema.parse(await request.json());

const updatedUser = await prisma.user.update({
data: { phone: payload.phone, name: payload.name },
where: { id: id }
});
const updatedProfile = await profileRepository.update({ id: id, ...payload });

return NextResponse.json(updatedUser);
};
return NextResponse.json(updatedProfile, { status: 200 });
} catch (error) {
if (error instanceof z.ZodError) return NextResponse.json({ errors: error.errors }, { status: 400 });

return NextResponse.json({ error: "Failed to create user" }, { status: 500 });
}
});

type PostReturn = NextResponse<{ image: string }>;

export const POST = async (
request: Request,
{ params }: { params: Promise<{ id: string }> }
): Promise<PostReturn> => {
export const POST = withAuthentication(async (request, ctx): Promise<PostReturn> => {
const { searchParams } = new URL(request.url);
const { id } = await params;
const { id } = await (ctx.params as unknown as Promise<{ id: string }>);

const filename = searchParams.get("filename");

Expand All @@ -45,4 +49,4 @@ export const POST = async (

const url = await prisma.user.update({ data: { image: blob.url }, where: { id: id } });
return NextResponse.json({ image: url.image! });
};
});
26 changes: 24 additions & 2 deletions src/applications/Profile/Api/deleteAccount.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
"use server";

import { HTTPError } from "ky";
import { pcomparatorApiClient } from "~/clients/PcomparatorApiClient";
import { auth } from "~/libraries/nextauth/authConfig";

/**
* `deleteAccount` deletes a user account.
* Deletes the authenticated user's account.
*
* @async
* @function deleteAccount
* @throws {Error} Throws an error if the user is not authenticated.
* @throws {Error} Throws an error with message "404 NOT FOUND" if the account is not found on the server.
* @throws {HTTPError} Re-throws any other HTTP errors encountered during the request.
* @returns {Promise<void>} Resolves to `void` upon successful account deletion.
*/
export const deleteAccount = async (): Promise<void> => {
const session = await auth();

await pcomparatorApiClient.delete(`user/${session?.user?.id}/account/delete`).json();
if (!session?.user?.id) throw new Error("User not authenticated");

try {
await pcomparatorApiClient.delete(`user/${session?.user?.id}/account/delete`).json();
} catch (err) {
if (err instanceof HTTPError) {
switch (err.response.status) {
case 404: {
console.log("Not Found");
throw new Error("404 NOT FOUND");
}
}
}
throw err;
}
};
53 changes: 38 additions & 15 deletions src/applications/Profile/Api/updateAvatar.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,59 @@
"use server";

import { HTTPError } from "ky";
import { z } from "zod";
import { pcomparatorApiClient } from "~/clients/PcomparatorApiClient";
import { pcomparatorAuthenticatedApiClient } from "~/clients/PcomparatorApiClient";
import { auth } from "~/libraries/nextauth/authConfig";

const ParamsSchema = z.object({
avatar: z.any()
image: z.instanceof(File)
});

const PayloadSchema = z.object({
image: z.string()
});

export type UpdateAvatarParams = z.infer<typeof ParamsSchema>;
export type UpdateAvatarPayload = z.infer<typeof PayloadSchema>;

/**
* `updateAvatar` updates a user's avatar
*
* Returns: user's avatar
* Updates the authenticated user's avatar.
*
* @async
* @function updateAvatar
* @param {z.infer<typeof ParamsSchema>} params - The parameters containing the image file to upload as the user's new avatar.
* @throws {Error} Throws an error if the user is not authenticated.
* @throws {Error} Throws an error with the message "404 NOT FOUND" if the user is not found on the server.
* @throws {HTTPError} Re-throws any other HTTP errors encountered during the request.
* @returns {Promise<UpdateAvatarPayload>} Resolves to an object containing the updated avatar image URL upon successful update.
*/
export const updateAvatar = async (params: z.infer<typeof ParamsSchema>): Promise<{ avatar: string }> => {
export const updateAvatar = async (params: z.infer<typeof ParamsSchema>): Promise<UpdateAvatarPayload> => {
const paramsPayload = ParamsSchema.parse(params);
const session = await auth();

const updatedUser = await pcomparatorApiClient
.post(`user/${session?.user?.id}/profile?filename=${paramsPayload.avatar.name}`, {
body: paramsPayload.avatar
})
.json();
if (!session?.user?.id) throw new Error("User not authenticated");

try {
const updatedUser = await pcomparatorAuthenticatedApiClient
.post(`user/${session.user.id}/profile?filename=${paramsPayload.image.name}`, {
body: paramsPayload.image
})
.json();

const updatedUserPayload = PayloadSchema.parse(updatedUser);
const updatedUserPayload = PayloadSchema.parse(updatedUser);

return {
avatar: updatedUserPayload.image
};
return {
image: updatedUserPayload.image
};
} catch (err) {
if (err instanceof HTTPError) {
switch (err.response.status) {
case 404: {
console.log("Not Found");
throw new Error("404 NOT FOUND");
}
}
}
throw err;
}
};
55 changes: 45 additions & 10 deletions src/applications/Profile/Api/updateFullname.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
"use server";

import { HTTPError } from "ky";
import { z } from "zod";
import { pcomparatorApiClient } from "~/clients/PcomparatorApiClient";
import type { Profile } from "~/applications/Profile/Domain/Entities/Profile";
import { pcomparatorAuthenticatedApiClient } from "~/clients/PcomparatorApiClient";
import { auth } from "~/libraries/nextauth/authConfig";

const ParamsSchema = z.object({
fullname: z.string()
name: z.string().min(0).max(32)
});

const PayloadSchema = z.object({
name: z.string().min(0).max(32)
});

export type UpdateFullnameParams = z.infer<typeof ParamsSchema>;
export type UpdateFullnamePayload = z.infer<typeof PayloadSchema>;

/**
* `updateFullname` updates the fullname of a user profile
* Updates the authenticated user's full name.
*
* Args: `fullname`: string
* @async
* @function updateFullname
* @param {z.infer<typeof ParamsSchema>} params - The parameters containing the new name to update in the user profile.
* @throws {Error} Throws an error if the user is not authenticated.
* @throws {Error} Throws an error with the message "404 NOT FOUND" if the user is not found on the server.
* @throws {HTTPError} Re-throws any other HTTP errors encountered during the request.
* @returns {Promise<UpdateFullnamePayload>} Resolves to an object containing the updated name upon successful update.
*/
export const updateFullname = async (params: z.infer<typeof ParamsSchema>): Promise<void> => {
export const updateFullname = async (
params: z.infer<typeof ParamsSchema>
): Promise<UpdateFullnamePayload> => {
const paramsPayload = ParamsSchema.parse(params);
const session = await auth();

await pcomparatorApiClient
.patch(`user/${session?.user?.id}/profile`, {
json: { name: paramsPayload.fullname }
})
.json();
if (!session?.user?.id) throw new Error("User not authenticated");

try {
const userPayload = PayloadSchema.parse(
await pcomparatorAuthenticatedApiClient
.patch(`user/${session.user.id}/profile`, {
json: { name: paramsPayload.name }
})
.json<Profile>()
);

return { name: userPayload.name };
} catch (err) {
if (err instanceof HTTPError) {
switch (err.response.status) {
case 404: {
console.log("Not Found");
throw new Error("404 NOT FOUND");
}
}
}
throw err;
}
};
Loading

0 comments on commit 8fd5315

Please sign in to comment.