Merge pull request #10 from levchak0910/improve-request
feat: improve request
This commit is contained in:
commit
6bc058b8ed
|
|
@ -2,8 +2,12 @@ import router from './routes'
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
|
||||
import registerGlobalComponents from './plugins/global-components'
|
||||
import { request } from './services'
|
||||
import parseStorageGet from './utils/parse-storage-get'
|
||||
|
||||
const token = parseStorageGet('user')?.token
|
||||
request.setAuthorizationHeader(token)
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { MutationTree } from 'vuex'
|
||||
import { Store } from './index'
|
||||
|
||||
import { request } from '../services'
|
||||
|
||||
export const MUTATION = {
|
||||
UPDATE_USER: 'UPDATE_USER',
|
||||
}
|
||||
|
|
@ -9,9 +11,11 @@ const mutations: MutationTree<Store> = {
|
|||
[MUTATION.UPDATE_USER] (state, user: User|null) {
|
||||
if (!user) {
|
||||
localStorage.removeItem('user')
|
||||
request.deleteAuthorizationHeader()
|
||||
state.user = null
|
||||
} else {
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
request.setAuthorizationHeader(user.token)
|
||||
state.user = user
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export default function params2query (params: Record<string, string | number | boolean>): string {
|
||||
return Object.entries(params).map(([key, value]) => `${key}=${value.toString()}`).join('&')
|
||||
}
|
||||
|
|
@ -1,208 +1,186 @@
|
|||
import FetchRequest from 'src/utils/request'
|
||||
|
||||
import params2query from 'src/utils/params-to-query'
|
||||
import mockFetch from 'src/utils/test/mock-fetch'
|
||||
import wrapTests from 'src/utils/test/wrap-tests'
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json () {
|
||||
return {}
|
||||
},
|
||||
})
|
||||
mockFetch({ type: 'body' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('# Request GET', function () {
|
||||
it('should implement GET method', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.get('/path')
|
||||
const PREFIX = '/prefix'
|
||||
const SUB_PREFIX = '/sub-prefix'
|
||||
const PATH = '/path'
|
||||
const PARAMS = { q1: 'q1', q2: 'q2' }
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(1)
|
||||
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||
method: 'GET',
|
||||
}))
|
||||
type SafeMethod = 'get' | 'delete'
|
||||
type UnsafeMethod = 'post' | 'put' | 'patch'
|
||||
type Method = SafeMethod | UnsafeMethod
|
||||
|
||||
const SAFE_METHODS: SafeMethod[] = ['get', 'delete']
|
||||
const UNSAFE_METHODS: UnsafeMethod[] = ['post', 'put', 'patch']
|
||||
|
||||
function isSafe (method: Method): method is SafeMethod {
|
||||
return ['get', 'delete'].includes(method)
|
||||
}
|
||||
|
||||
async function triggerMethod<T = any> (request: FetchRequest, method: Method, options?: any): Promise<T> {
|
||||
let body: T
|
||||
if (isSafe(method)) body = await request[method]<T>(PATH, options)
|
||||
else body = await request[method]<T>(PATH, {}, options)
|
||||
return body
|
||||
}
|
||||
|
||||
function forAllMethods (task: string, fn: (method: Method) => void): void {
|
||||
wrapTests<Method>({
|
||||
task,
|
||||
fn,
|
||||
list: [...UNSAFE_METHODS, ...SAFE_METHODS],
|
||||
testName: method => `for method: ${method}`,
|
||||
})
|
||||
}
|
||||
|
||||
forAllMethods('# Should be implemented', async (method) => {
|
||||
const request = new FetchRequest()
|
||||
|
||||
triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining({
|
||||
method: method.toUpperCase(),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('# Should implement prefix', () => {
|
||||
forAllMethods('should implement global prefix', async (method) => {
|
||||
const request = new FetchRequest({ prefix: PREFIX })
|
||||
|
||||
triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PREFIX}${PATH}`, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should can set prefix of request url with global', async function () {
|
||||
const request = new FetchRequest({ prefix: '/prefix' })
|
||||
await request.get('/path')
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||
})
|
||||
|
||||
it('should can be set prefix of request url with single request', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.get('/path', { prefix: '/prefix' })
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||
})
|
||||
|
||||
it('can be convert query object to query string in request url', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.get('/path', { params: { foo: 'bar' } })
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/path?foo=bar', expect.any(Object))
|
||||
})
|
||||
|
||||
it('should converted response body to json', async function () {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json () {
|
||||
return {
|
||||
foo: 'bar',
|
||||
}
|
||||
},
|
||||
})
|
||||
const request = new FetchRequest()
|
||||
const response = await request.get('/path')
|
||||
|
||||
expect(response).toMatchObject({ foo: 'bar' })
|
||||
})
|
||||
|
||||
it('should throw Error with response when request status code is not 2xx', async function () {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 400,
|
||||
statusText: 'Bad request',
|
||||
async json () {
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
forAllMethods('should implement local prefix', async (method) => {
|
||||
const request = new FetchRequest()
|
||||
|
||||
await expect(request.post('/path')).rejects.toThrow('Bad request')
|
||||
triggerMethod(request, method, { prefix: SUB_PREFIX })
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${SUB_PREFIX}${PATH}`, expect.any(Object))
|
||||
})
|
||||
|
||||
forAllMethods('should implement global + local prefix', async (method) => {
|
||||
const request = new FetchRequest({ prefix: PREFIX })
|
||||
|
||||
triggerMethod(request, method, { prefix: SUB_PREFIX })
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${SUB_PREFIX}${PATH}`, expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# Request POST', function () {
|
||||
it('should implement POST method', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.post('/path')
|
||||
describe('# Should convert query object to query string in request url', () => {
|
||||
forAllMethods('should implement global query', async (method) => {
|
||||
const request = new FetchRequest({ params: PARAMS })
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(1)
|
||||
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||
method: 'POST',
|
||||
}))
|
||||
triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PATH}?${params2query(PARAMS)}`, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should can set prefix of request url with global', async function () {
|
||||
const request = new FetchRequest({ prefix: '/prefix' })
|
||||
await request.post('/path')
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||
})
|
||||
|
||||
it('should can be set prefix of request url with single request', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.post('/path', {}, { prefix: '/prefix' })
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/prefix/path', expect.any(Object))
|
||||
})
|
||||
|
||||
it('should can be send json data in request body', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.post('/path', { foo: 'bar' })
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||
body: JSON.stringify({ foo: 'bar' }),
|
||||
}))
|
||||
})
|
||||
|
||||
it('can be convert query object to query string in request url', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.post('/path', {}, { params: { foo: 'bar' } })
|
||||
|
||||
expect(global.fetch).toBeCalledWith('/path?foo=bar', expect.any(Object))
|
||||
})
|
||||
|
||||
it('should converted response body to json', async function () {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json () {
|
||||
return {
|
||||
foo: 'bar',
|
||||
}
|
||||
},
|
||||
})
|
||||
const request = new FetchRequest()
|
||||
const response = await request.post('/path')
|
||||
|
||||
expect(response).toMatchObject({ foo: 'bar' })
|
||||
})
|
||||
|
||||
it('should throw Error with response when request status code is not 2xx', async function () {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 400,
|
||||
statusText: 'Bad request',
|
||||
async json () {
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
forAllMethods('should implement local query', async (method) => {
|
||||
const request = new FetchRequest()
|
||||
|
||||
await expect(request.post('/path')).rejects.toThrow('Bad request')
|
||||
triggerMethod(request, method, { params: PARAMS })
|
||||
|
||||
expect(global.fetch).toBeCalledWith(`${PATH}?${params2query(PARAMS)}`, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should throw Error with 4xx code', function () {
|
||||
global.fetch = jest.fn().mockReturnValue({
|
||||
ok: true,
|
||||
status: 422,
|
||||
async json () {
|
||||
return {
|
||||
errors: {
|
||||
some: ['error'],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
forAllMethods('should implement global + local query', async (method) => {
|
||||
const options = { params: { q1: 'q1', q2: 'q2' } }
|
||||
const localOptions = { params: { q1: 'q11', q3: 'q3' } }
|
||||
const expectedOptions = { params: { q1: 'q11', q2: 'q2', q3: 'q3' } }
|
||||
const request = new FetchRequest(options)
|
||||
|
||||
const request = new FetchRequest()
|
||||
triggerMethod(request, method, localOptions)
|
||||
|
||||
expect(() => {
|
||||
request.get('/')
|
||||
}).toThrow()
|
||||
expect(global.fetch).toBeCalledWith(`${PATH}?${params2query(expectedOptions.params)}`, expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# Request DELETE', function () {
|
||||
it('should implement DELETE method', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.delete('/path')
|
||||
describe('# Should work with headers', function () {
|
||||
forAllMethods('should add headers', async function (method) {
|
||||
const options = { headers: { h1: 'h1', h2: 'h2' } }
|
||||
const request = new FetchRequest(options)
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(1)
|
||||
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
}))
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(options))
|
||||
})
|
||||
|
||||
forAllMethods('should merge headers', async function (method) {
|
||||
const options = { headers: { h1: 'h1', h2: 'h2' } }
|
||||
const localOptions = { headers: { h1: 'h11', h3: 'h3' } }
|
||||
const expectedOptions = { headers: { h1: 'h11', h2: 'h2', h3: 'h3' } }
|
||||
const request = new FetchRequest(options)
|
||||
|
||||
await triggerMethod(request, method, localOptions)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(expectedOptions))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# Request PUT', function () {
|
||||
it('should implement PUT method', async function () {
|
||||
forAllMethods('# Should converted response body to json', async function (method) {
|
||||
const DATA = { foo: 'bar' }
|
||||
mockFetch({ type: 'body', ...DATA })
|
||||
const request = new FetchRequest()
|
||||
|
||||
const body = await triggerMethod(request, method)
|
||||
|
||||
expect(body).toMatchObject(DATA)
|
||||
})
|
||||
|
||||
forAllMethods('# Should throw Error with response when request status code is not 2xx', async function (method) {
|
||||
mockFetch({
|
||||
type: 'full',
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad request',
|
||||
json: async () => ({}),
|
||||
})
|
||||
const request = new FetchRequest()
|
||||
|
||||
await expect(triggerMethod(request, method)).rejects.toThrow('Bad request')
|
||||
})
|
||||
|
||||
describe('# Authorization header', function () {
|
||||
const TOKEN = 'token'
|
||||
const OPTIONS = { headers: { Authorization: `Token ${TOKEN}` } }
|
||||
|
||||
forAllMethods('should add authorization header', async function (method) {
|
||||
const request = new FetchRequest()
|
||||
await request.put('/path')
|
||||
request.setAuthorizationHeader(TOKEN)
|
||||
|
||||
triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(OPTIONS))
|
||||
})
|
||||
|
||||
forAllMethods('should remove authorization header', async function (method) {
|
||||
const request = new FetchRequest(OPTIONS)
|
||||
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(1)
|
||||
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||
method: 'PUT',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('# Request PATCH', function () {
|
||||
it('should implement PATCH method', async function () {
|
||||
const request = new FetchRequest()
|
||||
await request.patch('/path')
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(1)
|
||||
expect(global.fetch).toBeCalledWith('/path', expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining(OPTIONS))
|
||||
|
||||
request.deleteAuthorizationHeader()
|
||||
await triggerMethod(request, method)
|
||||
|
||||
expect(global.fetch).toBeCalledTimes(2)
|
||||
expect(global.fetch).toBeCalledWith(PATH, expect.objectContaining({
|
||||
headers: {},
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import parseStorageGet from './parse-storage-get'
|
||||
import merge from 'deepmerge'
|
||||
import params2query from './params-to-query'
|
||||
|
||||
interface FetchRequestOptions {
|
||||
prefix: string;
|
||||
|
|
@ -8,32 +9,33 @@ interface FetchRequestOptions {
|
|||
}
|
||||
|
||||
export default class FetchRequest {
|
||||
defaultOptions: FetchRequestOptions = {
|
||||
private defaultOptions: FetchRequestOptions = {
|
||||
prefix: '',
|
||||
headers: {},
|
||||
params: {},
|
||||
responseInterceptor: (response) => response,
|
||||
}
|
||||
|
||||
public options: FetchRequestOptions
|
||||
private options: FetchRequestOptions
|
||||
|
||||
constructor (options: Partial<FetchRequestOptions> = {}) {
|
||||
this.options = Object.assign({}, this.defaultOptions, options)
|
||||
this.options = merge(this.defaultOptions, options)
|
||||
}
|
||||
|
||||
private generateFinalUrl = (url: string, options: Partial<FetchRequestOptions> = {}) => {
|
||||
const prefix = options.prefix || this.options.prefix || ''
|
||||
const params = options.params || {}
|
||||
const prefix = options.prefix ?? this.options.prefix
|
||||
const params = merge(this.options.params, options.params ?? {})
|
||||
|
||||
let finalUrl = `${prefix}${url}`
|
||||
if (Object.keys(params).length) {
|
||||
const queryString = Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
|
||||
finalUrl += `?${queryString}`
|
||||
}
|
||||
if (Object.keys(params).length) finalUrl += `?${params2query(params)}`
|
||||
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
private generateFinalHeaders = (options: Partial<FetchRequestOptions> = {}) => {
|
||||
return merge(this.options.headers, options.headers ?? {})
|
||||
}
|
||||
|
||||
private handleResponse = (response: Response) => {
|
||||
this.options.responseInterceptor(response)
|
||||
return response.json()
|
||||
|
|
@ -51,74 +53,69 @@ export default class FetchRequest {
|
|||
}
|
||||
|
||||
get<T = any> (url: string, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||
options.headers = options.headers ?? {}
|
||||
const token = parseStorageGet('user')?.token
|
||||
if (token) options.headers.Authorization = `Token ${token}`
|
||||
|
||||
const finalUrl = this.generateFinalUrl(url, options)
|
||||
const headers = this.generateFinalHeaders(options)
|
||||
|
||||
return fetch(finalUrl, {
|
||||
method: 'GET',
|
||||
headers: this.options.headers,
|
||||
headers,
|
||||
})
|
||||
.then(this.handleResponse)
|
||||
}
|
||||
|
||||
post<T = any> (url: string, data: Record<string, any> = {}, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||
options.headers = options.headers ?? {}
|
||||
const token = parseStorageGet('user')?.token
|
||||
if (token) options.headers.Authorization = `Token ${token}`
|
||||
|
||||
const finalUrl = this.generateFinalUrl(url, options)
|
||||
const headers = this.generateFinalHeaders(options)
|
||||
|
||||
return fetch(finalUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: this.options.headers,
|
||||
headers,
|
||||
})
|
||||
.then(this.handleResponse)
|
||||
}
|
||||
|
||||
delete<T = any> (url: string, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||
options.headers = options.headers ?? {}
|
||||
const token = parseStorageGet('user')?.token
|
||||
if (token) options.headers.Authorization = `Token ${token}`
|
||||
|
||||
const finalUrl = this.generateFinalUrl(url, options)
|
||||
const headers = this.generateFinalHeaders(options)
|
||||
|
||||
return fetch(finalUrl, {
|
||||
method: 'DELETE',
|
||||
headers: this.options.headers,
|
||||
headers,
|
||||
})
|
||||
.then(this.handleResponse)
|
||||
}
|
||||
|
||||
put<T = any> (url: string, data: Record<string, any> = {}, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||
options.headers = options.headers ?? {}
|
||||
const token = parseStorageGet('user')?.token
|
||||
if (token) options.headers.Authorization = `Token ${token}`
|
||||
|
||||
const finalUrl = this.generateFinalUrl(url, options)
|
||||
const headers = this.generateFinalHeaders(options)
|
||||
|
||||
return fetch(finalUrl, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: this.options.headers,
|
||||
headers,
|
||||
})
|
||||
.then(this.handleResponse)
|
||||
}
|
||||
|
||||
patch<T = any> (url: string, data: Record<string, any> = {}, options: Partial<FetchRequestOptions> = {}): Promise<T> {
|
||||
options.headers = options.headers ?? {}
|
||||
const token = parseStorageGet('user')?.token
|
||||
if (token) options.headers.Authorization = `Token ${token}`
|
||||
|
||||
const finalUrl = this.generateFinalUrl(url, options)
|
||||
const headers = this.generateFinalHeaders(options)
|
||||
|
||||
return fetch(finalUrl, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
headers: this.options.headers,
|
||||
headers,
|
||||
})
|
||||
.then(this.handleResponse)
|
||||
}
|
||||
|
||||
public setAuthorizationHeader (token: string): void {
|
||||
if (!this.options.headers) this.options.headers = {}
|
||||
this.options.headers.Authorization = `Token ${token}`
|
||||
}
|
||||
|
||||
public deleteAuthorizationHeader (): void {
|
||||
delete this.options?.headers?.Authorization
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
interface FetchResponseBody {
|
||||
type: 'body'
|
||||
}
|
||||
interface FetchResponseFull {
|
||||
type: 'full'
|
||||
ok: boolean,
|
||||
status: number,
|
||||
statusText:string
|
||||
json: (...args: any) => Promise<any>
|
||||
}
|
||||
|
||||
export default function mockFetch (data: FetchResponseBody | FetchResponseFull): void {
|
||||
let response
|
||||
const { type, ...body } = data
|
||||
|
||||
if (type === 'body') {
|
||||
response = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => body,
|
||||
}
|
||||
} else {
|
||||
response = body
|
||||
}
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue(response)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
interface WrapTestsProps <Item> {
|
||||
task: string
|
||||
list: Item[]
|
||||
fn: (item: Item) => void,
|
||||
only?: boolean,
|
||||
testName?: (item: Item, index: number) => string
|
||||
}
|
||||
|
||||
function wrapTests<Item> ({ task, list, fn, testName, only = false }: WrapTestsProps<Item>): void {
|
||||
const descFn = only ? describe.only : describe
|
||||
|
||||
descFn(task, () => {
|
||||
list.forEach((item, index) => {
|
||||
const name = testName ? testName(item, index) : ''
|
||||
it(name, () => fn(item))
|
||||
})
|
||||
})
|
||||
}
|
||||
wrapTests.only = function <Item> ({ task, list, fn, testName }: WrapTestsProps<Item>): ReturnType<typeof wrapTests> {
|
||||
wrapTests({ task, list, fn, testName, only: true })
|
||||
}
|
||||
|
||||
export default wrapTests
|
||||
Loading…
Reference in New Issue