Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions apis/images.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { http, HttpResponse } from "msw";
import { server } from "@/mocks/server";
import { uploadImage, ROUTE_IMAGES, ROUTE_IMAGES_UPLOAD } from "./images";

const mockPresignedUrl = "https://s3.example.com/presigned";
const mockPublicUrl = "https://cdn.example.com/image.png";
const mockFileName = "test.png";
const mockFileType = "image/png";

const ENDPOINT_PRESIGNED = `/api${ROUTE_IMAGES}`;
const ENDPOINT_UPLOAD = `/api${ROUTE_IMAGES_UPLOAD}`;

function createMockFile(name: string, type: string) {
return new File(["dummy"], name, { type });
}

describe("uploadImage api 테스트", () => {
test("presigned URL 발급 후 S3 업로드에 성공하면 publicUrl을 반환함", async () => {
server.use(
http.post(ENDPOINT_PRESIGNED, () =>
HttpResponse.json(
{ presignedUrl: mockPresignedUrl, publicUrl: mockPublicUrl },
{ status: 200 },
),
),
http.put(ENDPOINT_UPLOAD, () => HttpResponse.json({}, { status: 200 })),
);

const file = createMockFile(mockFileName, mockFileType);
const result = await uploadImage(file);

expect(result).toBe(mockPublicUrl);
});

test("지원하지 않는 파일 형식이면 에러를 throw함", async () => {
const errorFileName = "test.svg";
const errorFileType = "image/svg";
const file = createMockFile(errorFileName, errorFileType);

await expect(uploadImage(file)).rejects.toThrow(
`'${errorFileType}'는 지원하지 않는 파일 형식입니다.`,
);
});

test("presigned URL 발급에 실패하면 에러를 throw함", async () => {
const errorResponse = { code: "PRESIGN_FAILED", message: "presigned URL 발급 실패" };
server.use(
http.post(ENDPOINT_PRESIGNED, () => HttpResponse.json(errorResponse, { status: 500 })),
);

const file = createMockFile(mockFileName, mockFileType);

await expect(uploadImage(file)).rejects.toThrow(errorResponse.message);
});

test("presigned URL 발급 실패 시 응답 바디 파싱이 안 되면 기본 에러 메시지를 throw함", async () => {
server.use(http.post(ENDPOINT_PRESIGNED, () => new HttpResponse(null, { status: 500 })));

const file = createMockFile(mockFileName, mockFileType);

await expect(uploadImage(file)).rejects.toThrow(
"업로드 주소 생성 중 알 수 없는 에러가 발생했습니다.",
);
});

test("S3 업로드에 실패하면 에러를 throw함", async () => {
const errorResponse = { code: "UPLOAD_FAILED", message: "S3 업로드 실패" };

server.use(
http.post(ENDPOINT_PRESIGNED, () =>
HttpResponse.json(
{ presignedUrl: mockPresignedUrl, publicUrl: mockPublicUrl },
{ status: 200 },
),
),
http.put(ENDPOINT_UPLOAD, () => HttpResponse.json(errorResponse, { status: 500 })),
);

const file = createMockFile(mockFileName, mockFileType);

await expect(uploadImage(file)).rejects.toThrow(errorResponse.message);
});

test("S3 업로드 실패 시 응답 바디 파싱이 안 되면 기본 에러 메시지를 throw함", async () => {
server.use(
http.post(ENDPOINT_PRESIGNED, () =>
HttpResponse.json(
{ presignedUrl: mockPresignedUrl, publicUrl: mockPublicUrl },
{ status: 200 },
),
),
http.put(ENDPOINT_UPLOAD, () => new HttpResponse(null, { status: 500 })),
);

const file = createMockFile(mockFileName, mockFileType);

await expect(uploadImage(file)).rejects.toThrow("업로드 중 알 수 없는 에러가 발생했습니다.");
});
});
4 changes: 2 additions & 2 deletions apis/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function uploadImage(file: File): Promise<string> {
}

/** 이미지 업로드 Step1: presigned URL 발급 */
const ROUTE_IMAGES = "/images/presigned";
export const ROUTE_IMAGES = "/images/presigned";
async function getPresignedUrl(fileName: string, contentType: string, folder: string = "meetings") {
const res = await clientFetch(ROUTE_IMAGES, {
method: "POST",
Expand All @@ -54,7 +54,7 @@ async function getPresignedUrl(fileName: string, contentType: string, folder: st
}

/** 이미지 업로드 Step2: S3에 이미지 업로드 */
const ROUTE_IMAGES_UPLOAD = "/images/upload";
export const ROUTE_IMAGES_UPLOAD = "/images/upload";
async function uploadToS3(presignedUrl: string, file: File) {
const res = await clientFetch(ROUTE_IMAGES_UPLOAD, {
method: "PUT",
Expand Down
80 changes: 80 additions & 0 deletions apis/meetingTypes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { http, HttpResponse } from "msw";
import { server } from "@/mocks/server";
import { BASE_URL } from "@/mocks/constants";
import {
getMeetingTypes,
initMeetingTypes,
ROUTE_MEETING_TYPES,
type MeetingTypeResponse,
} from "./meetingTypes";

const ENDPOINT_MEETING_TYPES = `${BASE_URL}${ROUTE_MEETING_TYPES}`;

const mockMeetingTypes: MeetingTypeResponse = [
{
id: 266,
createdAt: "2025-01-01T00:00:00Z",
name: "자기계발",
description: "게임, 코딩 등의 모임입니다.",
},
{
id: 267,
createdAt: "2025-01-01T00:00:00Z",
name: "운동/스포츠",
description: "런닝, 테니스 등의 모임입니다.",
},
];

describe("meetingTypes api 테스트", () => {
describe("getMeetingTypes", () => {
test("성공 시 카테고리 목록을 반환함", async () => {
server.use(
http.get(ENDPOINT_MEETING_TYPES, () =>
HttpResponse.json(mockMeetingTypes, { status: 200 }),
),
);

const result = await getMeetingTypes();

expect(result).toEqual(mockMeetingTypes);
});

test("실패 시 응답 메시지로 에러를 throw함", async () => {
const errorResponse = { code: "FETCH_FAILED", message: "카테고리 조회 실패" };

server.use(
http.get(ENDPOINT_MEETING_TYPES, () => HttpResponse.json(errorResponse, { status: 500 })),
);

await expect(getMeetingTypes()).rejects.toThrow(errorResponse.message);
});

test("실패 시 응답 바디 파싱이 안 되면 기본 에러 메시지를 throw함", async () => {
server.use(http.get(ENDPOINT_MEETING_TYPES, () => new HttpResponse(null, { status: 500 })));

await expect(getMeetingTypes()).rejects.toThrow("모임 카테고리 조회에 실패했습니다.");
});
});

describe("initMeetingTypes", () => {
test("성공 시 카테고리 목록을 반환함", async () => {
server.use(
http.get(ENDPOINT_MEETING_TYPES, () =>
HttpResponse.json(mockMeetingTypes, { status: 200 }),
),
);

const result = await initMeetingTypes();

expect(result).toEqual(mockMeetingTypes);
});

test("getMeetingTypes가 실패하면 null을 반환함", async () => {
server.use(http.get(ENDPOINT_MEETING_TYPES, () => new HttpResponse(null, { status: 500 })));

const result = await initMeetingTypes();

expect(result).toBeNull();
});
});
});
2 changes: 1 addition & 1 deletion apis/meetingTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ if (!BASE_URL) {
}

/** 모임 카테고리 목록 조회(빌드에서 실행) */
const ROUTE_MEETING_TYPES = "/meeting-types";
export const ROUTE_MEETING_TYPES = "/meeting-types";
export async function getMeetingTypes() {
const res = await fetch(`${BASE_URL}${ROUTE_MEETING_TYPES}`, {
method: "GET",
Expand Down
97 changes: 97 additions & 0 deletions apis/meetings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { http, HttpResponse } from "msw";
import { server } from "@/mocks/server";
import {
postMeetingsFavorite,
deleteMeetingsFavorite,
postMeetingsJoin,
deleteMeetingsJoin,
ROUTE_MEETINGS_FAVORITES,
ROUTE_MEETINGS_JOIN,
} from "./meetings";

const ENDPOINT_FAVORITES = (id: number) => `/api${ROUTE_MEETINGS_FAVORITES(id)}`;
const ENDPOINT_JOIN = (id: number) => `/api${ROUTE_MEETINGS_JOIN(id)}`;

describe("postMeetingsFavorite api 테스트", () => {
test("성공 시 응답 데이터를 반환함", async () => {
const result = await postMeetingsFavorite({ meetingId: 3 });

expect(result).toMatchObject({ id: 3, isFavorited: true });
});

test("존재하지 않는 모임이면 에러를 throw함", async () => {
await expect(postMeetingsFavorite({ meetingId: 9999 })).rejects.toThrow(
"모임을 찾을 수 없습니다.",
);
});

test("응답 바디가 없으면 기본 에러 메시지를 throw함", async () => {
server.use(http.post(ENDPOINT_FAVORITES(9999), () => new HttpResponse(null, { status: 500 })));

await expect(postMeetingsFavorite({ meetingId: 9999 })).rejects.toThrow(
"모임 찜 추가에 실패했습니다.",
);
});
});

describe("deleteMeetingsFavorite api 테스트", () => {
test("성공 시 응답 데이터를 반환함", async () => {
const result = await deleteMeetingsFavorite({ meetingId: 4 });

expect(result).toMatchObject({ message: "찜 해제 성공" });
});

test("존재하지 않는 모임이면 에러를 throw함", async () => {
await expect(deleteMeetingsFavorite({ meetingId: 9999 })).rejects.toThrow(
"모임을 찾을 수 없습니다.",
);
});

test("응답 바디가 없으면 기본 에러 메시지를 throw함", async () => {
server.use(
http.delete(ENDPOINT_FAVORITES(9999), () => new HttpResponse(null, { status: 500 })),
);

await expect(deleteMeetingsFavorite({ meetingId: 9999 })).rejects.toThrow(
"모임 찜 해제에 실패했습니다.",
);
});
});

describe("postMeetingsJoin api 테스트", () => {
test("이미 참여한 모임이면 에러를 throw함", async () => {
await expect(postMeetingsJoin({ meetingId: 1 })).rejects.toThrow("ALREADY_JOINED");
});

test("존재하지 않는 모임이면 에러를 throw함", async () => {
await expect(postMeetingsJoin({ meetingId: 9999 })).rejects.toThrow("NOT_FOUND");
});

test("응답 바디가 없으면 기본 에러 메시지를 throw함", async () => {
server.use(http.post(ENDPOINT_JOIN(9999), () => new HttpResponse(null, { status: 500 })));

await expect(postMeetingsJoin({ meetingId: 9999 })).rejects.toThrow(
"모임 참여에 실패했습니다.",
);
});
});

describe("deleteMeetingsJoin api 테스트", () => {
test("성공 시 응답 데이터를 반환함", async () => {
const result = await deleteMeetingsJoin({ meetingId: 5 });

expect(result).toMatchObject({ message: "참여 취소 성공" });
});

test("존재하지 않는 모임이면 에러를 throw함", async () => {
await expect(deleteMeetingsJoin({ meetingId: 9999 })).rejects.toThrow("NOT_FOUND");
});

test("응답 바디가 없으면 기본 에러 메시지를 throw함", async () => {
server.use(http.delete(ENDPOINT_JOIN(9999), () => new HttpResponse(null, { status: 500 })));

await expect(deleteMeetingsJoin({ meetingId: 9999 })).rejects.toThrow(
"모임 참여 취소에 실패했습니다.",
);
});
});
4 changes: 2 additions & 2 deletions apis/meetings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clientFetch } from "@/libs/clientFetch";

const ROUTE_MEETINGS_FAVORITES = (meetingId: number) => `/meetings/${meetingId}/favorites`;
const ROUTE_MEETINGS_JOIN = (meetingId: number) => `/meetings/${meetingId}/join`;
export const ROUTE_MEETINGS_FAVORITES = (meetingId: number) => `/meetings/${meetingId}/favorites`;
export const ROUTE_MEETINGS_JOIN = (meetingId: number) => `/meetings/${meetingId}/join`;

/** 모임 찜 추가 */
export async function postMeetingsFavorite({ meetingId }: { meetingId: number }): Promise<void> {
Expand Down
Loading
Loading