Skip to content

Commit

Permalink
✨ Introduce an apiClient
Browse files Browse the repository at this point in the history
  • Loading branch information
0x46616c6b committed May 19, 2024
1 parent 1bca36c commit 3338409
Show file tree
Hide file tree
Showing 57 changed files with 2,086 additions and 558 deletions.
33 changes: 33 additions & 0 deletions src/api/Api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { apiClient, apiHeaders } from './Api'

describe('apiClient', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { features: { telegramEnabled: true } } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await apiClient('/admin/features', { headers: apiHeaders('token') })
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith('/admin/features', { headers: apiHeaders('token') })
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await apiClient('/admin/features', { headers: apiHeaders('token') })
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith('/admin/features', { headers: apiHeaders('token') })
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await apiClient('/admin/features', { headers: apiHeaders('token') })
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith('/admin/features', { headers: apiHeaders('token') })
})
})
39 changes: 37 additions & 2 deletions src/api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,46 @@ type StatusSuccess = 'success'
type StatusError = 'error'
type Status = StatusSuccess | StatusError

export interface Response<T> {
data: T
export interface ApiResponse<T> {
data?: T
status: Status
error?: {
code: number
message: string
}
}

export async function apiClient<T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
try {
const response = await fetch(url, options)

if (!response.ok) {
const message = await response.text()
throw new Error(`HTTP error! status: ${response.status}, message: ${message}`)
}

const data: ApiResponse<T> = await response.json()

if (data.status === 'error') {
throw new Error(data.error?.message || 'Unknown error')
}

return data
} catch (error) {
return {
status: 'error',
error: {
code: 500,
message: (error as Error).message || 'Unknown error',
},
}
}
}

export function apiHeaders(token: string): HeadersInit {
return {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
}
}
17 changes: 8 additions & 9 deletions src/api/Auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { login } from './Auth'

describe('Auth', function () {
describe('Auth', () => {
beforeEach(() => {
fetch.resetMocks()
fetch.doMock()
fetchMock.resetMocks()
})

test('login failed', function () {
fetch.mockResponse(
it('should fail to login when credentials wrong', () => {
fetchMock.mockResponse(
JSON.stringify({
data: {},
status: 'error',
Expand All @@ -19,19 +18,19 @@ describe('Auth', function () {
expect(login('[email protected]', 'password')).rejects.toThrow('Login failed')
})

test('server error', function () {
fetch.mockReject()
it('should fail when network fails', () => {
fetchMock.mockReject()

expect(login('[email protected]', 'password')).rejects.toThrow('Login failed')
})

test('login successful', function () {
it('should succeed', () => {
const response = {
code: 200,
expire: '2022-10-01T18:22:37+02:00',
token: 'token',
}
fetch.mockResponse(JSON.stringify(response), { status: 200 })
fetchMock.mockResponse(JSON.stringify(response), { status: 200 })

expect(login('[email protected]', 'password')).resolves.toEqual(response)
})
Expand Down
34 changes: 34 additions & 0 deletions src/api/Features.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApiUrl, apiHeaders } from './Api'
import { fetchFeaturesApi } from './Features'

describe('fetchFeaturesApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { features: { telegramEnabled: true } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchFeaturesApi('token')
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/features`, { headers: apiHeaders('token') })
})

it('should return data on error', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchFeaturesApi('token')
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/features`, { headers: apiHeaders('token') })
})

it('should return data on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await fetchFeaturesApi('token')
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/features`, { headers: apiHeaders('token') })
})
})
18 changes: 3 additions & 15 deletions src/api/Features.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiUrl, Response } from './Api'
import { ApiResponse, ApiUrl, apiClient, apiHeaders } from './Api'

interface FeaturesResponseData {
features: Features
Expand All @@ -8,18 +8,6 @@ export interface Features {
telegramEnabled: boolean
}

export function useFeatureApi(token: string) {
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
}

const getFeatures = (): Promise<Response<FeaturesResponseData>> => {
return fetch(`${ApiUrl}/admin/features`, {
headers: headers,
}).then(response => response.json())
}

return { getFeatures }
export async function fetchFeaturesApi(token: string): Promise<ApiResponse<FeaturesResponseData>> {
return apiClient<FeaturesResponseData>(`${ApiUrl}/admin/features`, { headers: apiHeaders(token) })
}
167 changes: 167 additions & 0 deletions src/api/Message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ApiUrl, apiHeaders } from './Api'
import { Message, deleteMessageApi, fetchMessagesApi, postMessageApi } from './Message'

describe('fetchMessagesApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { messages: [] } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchMessagesApi('token', 1)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=10`, { headers: apiHeaders('token') })
})

it('should return data with before parameter', async () => {
const data = { status: 'success', data: { messages: [] } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchMessagesApi('token', 1, 2)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=10&before=2`, { headers: apiHeaders('token') })
})

it('should return data with after parameter', async () => {
const data = { status: 'success', data: { messages: [] } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchMessagesApi('token', 1, undefined, 3)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=10&after=3`, { headers: apiHeaders('token') })
})

it('should return data with before and after parameters', async () => {
const data = { status: 'success', data: { messages: [] } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchMessagesApi('token', 1, 2, 3)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=10&before=2&after=3`, { headers: apiHeaders('token') })
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchMessagesApi('token', 1)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=10`, { headers: apiHeaders('token') })
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await fetchMessagesApi('token', 1)
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=10`, { headers: apiHeaders('token') })
})

it('should return data with custom limit', async () => {
const data = { status: 'success', data: { messages: [] } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await fetchMessagesApi('token', 1, undefined, undefined, 5)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages?limit=5`, { headers: apiHeaders('token') })
})
})

describe('postMessageApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { message: {} } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await postMessageApi('token', 'ticker', 'text', { type: 'FeatureCollection', features: [] }, [])
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/ticker/messages`, {
headers: apiHeaders('token'),
method: 'post',
body: JSON.stringify({
text: 'text',
geoInformation: { type: 'FeatureCollection', features: [] },
attachments: [],
}),
})
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await postMessageApi('token', 'ticker', 'text', { type: 'FeatureCollection', features: [] }, [])
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/ticker/messages`, {
headers: apiHeaders('token'),
method: 'post',
body: JSON.stringify({
text: 'text',
geoInformation: { type: 'FeatureCollection', features: [] },
attachments: [],
}),
})
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await postMessageApi('token', 'ticker', 'text', { type: 'FeatureCollection', features: [] }, [])
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/ticker/messages`, {
headers: apiHeaders('token'),
method: 'post',
body: JSON.stringify({
text: 'text',
geoInformation: { type: 'FeatureCollection', features: [] },
attachments: [],
}),
})
})
})

describe('deleteMessageApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success' }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await deleteMessageApi('token', { id: 1, ticker: 1 } as Message)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages/1`, {
headers: apiHeaders('token'),
method: 'delete',
})
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await deleteMessageApi('token', { id: 1, ticker: 1 } as Message)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages/1`, {
headers: apiHeaders('token'),
method: 'delete',
})
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await deleteMessageApi('token', { id: 1, ticker: 1 } as Message)
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/messages/1`, {
headers: apiHeaders('token'),
method: 'delete',
})
})
})
Loading

0 comments on commit 3338409

Please sign in to comment.