Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Introduce an apiClient #634

Merged
merged 1 commit into from
May 19, 2024
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
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('user@systemli.org', 'password')).rejects.toThrow('Login failed')
})

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

expect(login('user@systemli.org', '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('user@systemli.org', '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
Loading