-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from GDSC-Hongik/feature/fetch-setting
[Feature] fetch 클래스 및 테스트 코드 작성
- Loading branch information
Showing
12 changed files
with
1,748 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/** @type {import("eslint").Linter.Config} */ | ||
module.exports = { | ||
root: true, | ||
extends: ["@wow-class/eslint-config/basic.js"], | ||
parser: "@typescript-eslint/parser", | ||
parserOptions: { | ||
project: true, | ||
ecmaVersion: 2020, | ||
sourceType: "module", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
setupFiles: ["<rootDir>/jest.setup.ts"], | ||
moduleFileExtensions: ["ts", "tsx", "js", "jsx"], | ||
transform: { | ||
"^.+\\.ts?$": "ts-jest", | ||
}, | ||
testMatch: ["<rootDir>/**/*.(test|spec).ts"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import fetchMock from "jest-fetch-mock"; | ||
|
||
fetchMock.enableMocks(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "@wow-class/utils", | ||
"version": "0.0.0", | ||
"private": true, | ||
"scripts": { | ||
"test": "jest" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^29.5.12", | ||
"jest": "^29.7.0", | ||
"jest-fetch-mock": "^3.0.3", | ||
"ts-jest": "^29.2.4", | ||
"@wow-class/typescript-config": "workspace:*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import fetchMock from "jest-fetch-mock"; | ||
|
||
import { fetcher } from "./fetcher"; | ||
|
||
describe("Fetcher", () => { | ||
beforeEach(() => { | ||
fetchMock.resetMocks(); | ||
}); | ||
|
||
it("should set the baseURL correctly", () => { | ||
fetcher.setBaseUrl("https://api.example.com"); | ||
|
||
expect(fetcher["baseUrl"]).toBe("https://api.example.com"); | ||
}); | ||
|
||
it("should set default headers correctly", () => { | ||
fetcher.setDefaultHeaders({ Authorization: "Bearer test-token" }); | ||
|
||
expect(fetcher["defaultHeaders"]).toEqual({ | ||
Authorization: "Bearer test-token", | ||
}); | ||
}); | ||
|
||
it("should make a GET request with the correct headers and URL", async () => { | ||
fetchMock.mockResponseOnce(JSON.stringify({ success: true })); | ||
fetcher.setBaseUrl("https://api.example.com"); | ||
fetcher.setDefaultHeaders({ | ||
"Content-Type": "application/json", | ||
Authorization: "Bearer test-token", | ||
}); | ||
|
||
const response = await fetcher.get("/test-endpoint"); | ||
|
||
expect(fetchMock).toHaveBeenCalledWith( | ||
"https://api.example.com/test-endpoint", | ||
{ | ||
method: "GET", | ||
headers: { | ||
Authorization: "Bearer test-token", | ||
"Content-Type": "application/json", | ||
}, | ||
} | ||
); | ||
const jsonData = JSON.parse(response.data); | ||
expect(jsonData).toEqual({ success: true }); | ||
}); | ||
|
||
it("should make a GET request with query parameters", async () => { | ||
fetchMock.mockResponseOnce(JSON.stringify({ success: true })); | ||
fetcher.setBaseUrl("https://api.example.com"); | ||
fetcher.setDefaultHeaders({ | ||
"Content-Type": "application/json", | ||
Authorization: "Bearer test-token", | ||
}); | ||
|
||
const params = { key1: "value1", key2: "value2" }; | ||
const response = await fetcher.get("/test-endpoint", {}, params); | ||
|
||
expect(fetchMock).toHaveBeenCalledWith( | ||
"https://api.example.com/test-endpoint?key1=value1&key2=value2", | ||
{ | ||
method: "GET", | ||
headers: { | ||
Authorization: "Bearer test-token", | ||
"Content-Type": "application/json", | ||
}, | ||
} | ||
); | ||
const jsonData = JSON.parse(response.data); | ||
expect(jsonData).toEqual({ success: true }); | ||
}); | ||
|
||
it("should make a POST request with the correct headers and URL and body", async () => { | ||
fetchMock.mockResponseOnce(JSON.stringify({ success: true })); | ||
fetcher.setBaseUrl("https://api.example.com"); | ||
fetcher.setDefaultHeaders({ | ||
"Content-Type": "application/json", | ||
Authorization: "Bearer test-token", | ||
}); | ||
|
||
const response = await fetcher.post("/test-endpoint", { foo: "bar" }); | ||
|
||
expect(fetchMock).toHaveBeenCalledWith( | ||
"https://api.example.com/test-endpoint", | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: "Bearer test-token", | ||
}, | ||
body: JSON.stringify({ foo: "bar" }), | ||
} | ||
); | ||
const jsonData = JSON.parse(response.data); | ||
expect(jsonData).toEqual({ success: true }); | ||
}); | ||
|
||
it("should handle plain text responses", async () => { | ||
fetchMock.mockResponseOnce("plain text response", { | ||
headers: { "Content-Type": "text/plain" }, | ||
}); | ||
fetcher.setBaseUrl("https://api.example.com"); | ||
|
||
const response = await fetcher.get("/test-endpoint"); | ||
expect(response.data).toBe("plain text response"); | ||
}); | ||
|
||
it("should handle HTTP errors correctly", async () => { | ||
fetchMock.mockResponseOnce("Not Found", { status: 404 }); | ||
fetcher.setBaseUrl("https://api.example.com"); | ||
fetcher.setDefaultHeaders({ | ||
"Content-Type": "application/json", | ||
Authorization: "Bearer test-token", | ||
}); | ||
|
||
try { | ||
await fetcher.get("/test-endpoint"); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(Error); | ||
expect((error as any).response).toBeInstanceOf(Response); | ||
expect((error as any).response.status).toBe(404); | ||
expect((error as any).responseText).toBe("Not Found"); | ||
expect((error as any).message).toBe("HTTP Error: 404 Not Found"); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
type ApiResponse<T = any> = Response & { data?: T }; | ||
|
||
type RequestInterceptor = ( | ||
options: RequestInit | ||
) => RequestInit | Promise<RequestInit>; | ||
type ResponseInterceptor<T = any> = ( | ||
response: ApiResponse | ||
) => ApiResponse<T> | Promise<ApiResponse<T>>; | ||
|
||
class Fetcher { | ||
private baseUrl: string; | ||
private defaultHeaders: HeadersInit; | ||
private requestInterceptors: RequestInterceptor[]; | ||
private responseInterceptors: ResponseInterceptor[]; | ||
|
||
constructor({ baseUrl = "", defaultHeaders = {} } = {}) { | ||
this.baseUrl = baseUrl; | ||
this.defaultHeaders = defaultHeaders; | ||
this.requestInterceptors = []; | ||
this.responseInterceptors = []; | ||
} | ||
|
||
setBaseUrl(baseUrl: string) { | ||
this.baseUrl = baseUrl; | ||
} | ||
|
||
setDefaultHeaders(headers: HeadersInit) { | ||
this.defaultHeaders = headers; | ||
} | ||
|
||
addRequestInterceptor(interceptor: RequestInterceptor) { | ||
this.requestInterceptors.push(interceptor); | ||
} | ||
|
||
addResponseInterceptor<T = any>(interceptor: ResponseInterceptor<T>) { | ||
this.responseInterceptors.push(interceptor); | ||
} | ||
|
||
private async interceptRequest(options: RequestInit): Promise<RequestInit> { | ||
options.headers = { ...this.defaultHeaders, ...options.headers }; | ||
|
||
for (const interceptor of this.requestInterceptors) { | ||
options = (await interceptor(options)) || options; | ||
} | ||
|
||
return options; | ||
} | ||
|
||
private async interceptResponse<T = any>( | ||
response: Response | ||
): Promise<ApiResponse<T>> { | ||
for (const interceptor of this.responseInterceptors) { | ||
response = (await interceptor(response)) || response; | ||
} | ||
|
||
return response; | ||
} | ||
|
||
private async parseJsonResponse(response: Response): Promise<any> { | ||
const contentType = response.headers.get("Content-Type") || ""; | ||
|
||
if (contentType.includes("application/json")) { | ||
return response.json(); | ||
} else if (contentType.startsWith("image/")) { | ||
return response.blob(); | ||
} | ||
|
||
return response.text(); | ||
} | ||
|
||
private async handleError(response: Response) { | ||
if (!response.ok) { | ||
const text = await response.text(); | ||
const error = new Error( | ||
`HTTP Error: ${response.status} ${response.statusText}` | ||
); | ||
(error as any).response = response; | ||
(error as any).responseText = text; | ||
|
||
throw error; | ||
} | ||
} | ||
|
||
async request<T = any>( | ||
url: string, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
options = await this.interceptRequest(options); | ||
|
||
const fullUrl = this.baseUrl + url; | ||
|
||
let response: ApiResponse = await fetch(fullUrl, options); | ||
|
||
await this.handleError(response); | ||
|
||
response = await this.interceptResponse(response); | ||
response.data = await this.parseJsonResponse(response); | ||
|
||
return response; | ||
} | ||
|
||
get<T = any>( | ||
url: string, | ||
options: RequestInit = {}, | ||
params: Record<string, any> = {} | ||
): Promise<ApiResponse<T>> { | ||
const queryString = | ||
params && Object.keys(params).length > 0 | ||
? `?${new URLSearchParams(params).toString()}` | ||
: ""; | ||
const fullUrl = `${url}${queryString}`; | ||
|
||
return this.request(fullUrl, { ...options, method: "GET" }); | ||
} | ||
|
||
post<T = any>( | ||
url: string, | ||
body: any, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "POST", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
|
||
put<T = any>( | ||
url: string, | ||
body: any, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "PUT", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
|
||
patch<T = any>( | ||
url: string, | ||
body: any, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "PATCH", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
|
||
delete<T = any>( | ||
url: string, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { ...options, method: "DELETE" }); | ||
} | ||
} | ||
|
||
export const fetcher = new Fetcher({ | ||
baseUrl: | ||
process.env.NODE_ENV === "production" | ||
? process.env.NEXT_PUBLIC_PROD_BASE_URL | ||
: process.env.NEXT_PUBLIC_DEV_BASE_URL, | ||
defaultHeaders: { "Content-Type": "application/json" }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extends": "@wow-class/typescript-config/basic.json", | ||
"include": ["src", "jest.setup.ts"], | ||
"exclude": ["node_modules"] | ||
} |
Oops, something went wrong.