Build NapCat Artifacts / Windows Loader Test (push) Canceled after 0s
Build NapCat Artifacts / Type Check & Test (push) Canceled after 0s
Build NapCat Artifacts / Build NapCat.Framework (push) Canceled after 0s
Build NapCat Artifacts / Build NapCat.Shell (push) Canceled after 0s
1566 lines
45 KiB
Plaintext
1566 lines
45 KiB
Plaintext
import { createSlice } from '@reduxjs/toolkit'
|
|
import type { FetchArgs } from '@reduxjs/toolkit/query'
|
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
|
|
import { headersToObject } from 'headers-polyfill'
|
|
import { HttpResponse, delay, http } from 'msw'
|
|
// @ts-ignore
|
|
import nodeFetch from 'node-fetch'
|
|
import queryString from 'query-string'
|
|
import { vi } from 'vitest'
|
|
import { setupApiStore } from '../../tests/utils/helpers'
|
|
import type { BaseQueryApi } from '../baseQueryTypes'
|
|
import { server } from './mocks/server'
|
|
|
|
const defaultHeaders: Record<string, string> = {
|
|
fake: 'header',
|
|
delete: 'true',
|
|
delete2: '1',
|
|
}
|
|
|
|
const baseUrl = 'https://example.com'
|
|
|
|
// @ts-ignore
|
|
const fetchFn = vi.fn<Promise<any>, any[]>(nodeFetch)
|
|
|
|
const baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (headers, { getState }) => {
|
|
const { token } = (getState() as RootState).auth
|
|
|
|
// If we have a token set in state, let's assume that we should be passing it.
|
|
if (token) {
|
|
headers.set('authorization', `Bearer ${token}`)
|
|
}
|
|
// A user could customize their behavior here, so we'll just test that custom scenarios would work.
|
|
const potentiallyConflictingKeys = Object.keys(defaultHeaders)
|
|
potentiallyConflictingKeys.forEach((key) => {
|
|
// Check for presence of a default key, if the incoming endpoint headers don't specify it as '', then set it
|
|
const existingValue = headers.get(key)
|
|
if (!existingValue && existingValue !== '') {
|
|
headers.set(key, String(defaultHeaders[key]))
|
|
// If an endpoint sets a header with a value of '', just delete the header.
|
|
} else if (headers.get(key) === '') {
|
|
headers.delete(key)
|
|
}
|
|
})
|
|
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const api = createApi({
|
|
baseQuery,
|
|
endpoints(build) {
|
|
return {
|
|
query: build.query({ query: () => ({ url: '/echo', headers: {} }) }),
|
|
mutation: build.mutation({
|
|
query: () => ({ url: '/echo', method: 'POST', credentials: 'omit' }),
|
|
}),
|
|
}
|
|
},
|
|
})
|
|
|
|
const authSlice = createSlice({
|
|
name: 'auth',
|
|
initialState: {
|
|
token: '',
|
|
},
|
|
reducers: {
|
|
setToken(state, action) {
|
|
state.token = action.payload
|
|
},
|
|
},
|
|
})
|
|
|
|
const storeRef = setupApiStore(api, { auth: authSlice.reducer })
|
|
type RootState = ReturnType<typeof storeRef.store.getState>
|
|
|
|
let commonBaseQueryApi: BaseQueryApi = {} as any
|
|
beforeEach(() => {
|
|
let abortController = new AbortController()
|
|
commonBaseQueryApi = {
|
|
signal: abortController.signal,
|
|
abort: (reason) =>
|
|
// @ts-ignore
|
|
abortController.abort(reason),
|
|
dispatch: storeRef.store.dispatch,
|
|
getState: storeRef.store.getState,
|
|
extra: undefined,
|
|
type: 'query',
|
|
endpoint: 'doesntmatterhere',
|
|
}
|
|
})
|
|
|
|
describe('fetchBaseQuery', () => {
|
|
describe('basic functionality', () => {
|
|
it('should return an object for a simple GET request when it is json data', async () => {
|
|
const req = baseQuery('/success', commonBaseQueryApi, {})
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.data).toEqual({ value: 'success' })
|
|
})
|
|
|
|
it('should return undefined for a simple GET request when the response is empty', async () => {
|
|
const req = baseQuery('/empty', commonBaseQueryApi, {})
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
|
|
expect(res.data).toBeNull()
|
|
})
|
|
|
|
it('should return an error and status for error responses', async () => {
|
|
const req = baseQuery('/error', commonBaseQueryApi, {})
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.error).toEqual({
|
|
status: 500,
|
|
data: { value: 'error' },
|
|
})
|
|
})
|
|
|
|
it('should handle a connection loss semi-gracefully', async () => {
|
|
fetchFn.mockRejectedValueOnce(new TypeError('Failed to fetch'))
|
|
|
|
const req = baseQuery('/success', commonBaseQueryApi, {})
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBe(undefined)
|
|
expect(res.error).toEqual({
|
|
status: 'FETCH_ERROR',
|
|
error: 'TypeError: Failed to fetch',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('non-JSON-body', () => {
|
|
it('success: should return data ("text" responseHandler)', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.text(`this is not json!`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const req = baseQuery(
|
|
{ url: '/success', responseHandler: 'text' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.data).toEqual(`this is not json!`)
|
|
})
|
|
|
|
it('success: should fail gracefully (default="json" responseHandler)', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.text(`this is not json!`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const req = baseQuery('/success', commonBaseQueryApi, {})
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.error).toEqual({
|
|
status: 'PARSING_ERROR',
|
|
error: expect.stringMatching(/SyntaxError: Unexpected token/),
|
|
originalStatus: 200,
|
|
data: `this is not json!`,
|
|
})
|
|
})
|
|
|
|
it('success: parse text without error ("content-type" responseHandler)', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.text(`this is not json!`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const req = baseQuery(
|
|
{
|
|
url: '/success',
|
|
responseHandler: 'content-type',
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'text/plain',
|
|
)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.data).toEqual(`this is not json!`)
|
|
})
|
|
|
|
it('success: parse json without error ("content-type" responseHandler)', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.json(`this will become json!`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const req = baseQuery(
|
|
{
|
|
url: '/success',
|
|
responseHandler: 'content-type',
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'application/json',
|
|
)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.data).toEqual(`this will become json!`)
|
|
})
|
|
|
|
it('server error: should fail normally with a 500 status ("text" responseHandler)', async () => {
|
|
server.use(
|
|
http.get('https://example.com/error', () =>
|
|
HttpResponse.text(`this is not json!`, { status: 500 }),
|
|
),
|
|
)
|
|
|
|
const req = baseQuery(
|
|
{ url: '/error', responseHandler: 'text' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.error).toEqual({
|
|
status: 500,
|
|
data: `this is not json!`,
|
|
})
|
|
})
|
|
|
|
it('server error: should fail normally with a 500 status as text ("content-type" responseHandler)', async () => {
|
|
const serverResponse = 'Internal Server Error'
|
|
server.use(
|
|
http.get('https://example.com/error', () =>
|
|
HttpResponse.text(serverResponse, { status: 500 }),
|
|
),
|
|
)
|
|
|
|
const req = baseQuery(
|
|
{ url: '/error', responseHandler: 'content-type' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'text/plain',
|
|
)
|
|
expect(res.error).toEqual({
|
|
status: 500,
|
|
data: serverResponse,
|
|
})
|
|
})
|
|
|
|
it('server error: should fail normally with a 500 status as json ("content-type" responseHandler)', async () => {
|
|
const serverResponse = {
|
|
errors: { field1: "Password cannot be 'password'" },
|
|
}
|
|
server.use(
|
|
http.get('https://example.com/error', () =>
|
|
HttpResponse.json(serverResponse, { status: 500 }),
|
|
),
|
|
)
|
|
|
|
const req = baseQuery(
|
|
{ url: '/error', responseHandler: 'content-type' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'application/json',
|
|
)
|
|
expect(res.error).toEqual({
|
|
status: 500,
|
|
data: serverResponse,
|
|
})
|
|
})
|
|
|
|
it('server error: should fail gracefully (default="json" responseHandler)', async () => {
|
|
server.use(
|
|
http.get('https://example.com/error', () =>
|
|
HttpResponse.text(`this is not json!`, { status: 500 }),
|
|
),
|
|
)
|
|
|
|
const req = baseQuery('/error', commonBaseQueryApi, {})
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.error).toEqual({
|
|
status: 'PARSING_ERROR',
|
|
error: expect.stringMatching(/SyntaxError: Unexpected token/),
|
|
originalStatus: 500,
|
|
data: `this is not json!`,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('arg.body', () => {
|
|
test('an object provided to body will be serialized when content-type is json', async () => {
|
|
const data = {
|
|
test: 'value',
|
|
}
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', body: data, method: 'POST' },
|
|
{ ...commonBaseQueryApi, type: 'mutation' },
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('application/json')
|
|
expect(request.body).toEqual(data)
|
|
})
|
|
|
|
test('an array provided to body will be serialized when content-type is json', async () => {
|
|
const data = ['test', 'value']
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', body: data, method: 'POST' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('application/json')
|
|
expect(request.body).toEqual(data)
|
|
})
|
|
|
|
test('an object provided to body will not be serialized when content-type is not json', async () => {
|
|
const data = {
|
|
test: 'value',
|
|
}
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
body: data,
|
|
method: 'POST',
|
|
headers: { 'content-type': 'text/html' },
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('text/html')
|
|
expect(request.body).toEqual('[object Object]')
|
|
})
|
|
|
|
test('an array provided to body will not be serialized when content-type is not json', async () => {
|
|
const data = ['test', 'value']
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
body: data,
|
|
method: 'POST',
|
|
headers: { 'content-type': 'text/html' },
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('text/html')
|
|
expect(request.body).toEqual(data.join(','))
|
|
})
|
|
|
|
it('supports a custom jsonContentType', async () => {
|
|
const baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
jsonContentType: 'application/vnd.api+json',
|
|
})
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
body: {},
|
|
method: 'POST',
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('application/vnd.api+json')
|
|
})
|
|
|
|
it('supports a custom jsonReplacer', async () => {
|
|
const body = {
|
|
items: new Set(['A', 'B', 'C']),
|
|
}
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
body,
|
|
method: 'POST',
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('application/json')
|
|
expect(request.body).toEqual({ items: {} }) // Set is not properly marshalled by default
|
|
|
|
// Use jsonReplacer
|
|
const baseQueryWithReplacer = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
jsonReplacer: (key, value) =>
|
|
value instanceof Set ? [...value] : value,
|
|
})
|
|
|
|
;({ data: request } = await baseQueryWithReplacer(
|
|
{
|
|
url: '/echo',
|
|
body,
|
|
method: 'POST',
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['content-type']).toBe('application/json')
|
|
expect(request.body).toEqual({ items: ['A', 'B', 'C'] }) // Set is marshalled correctly by jsonReplacer
|
|
})
|
|
})
|
|
|
|
describe('arg.params', () => {
|
|
it('should not serialize missing params', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(`${baseUrl}/echo`)
|
|
})
|
|
|
|
it('should serialize numeric and boolean params', async () => {
|
|
const params = { a: 1, b: true }
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', params },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(`${baseUrl}/echo?a=1&b=true`)
|
|
})
|
|
|
|
it('should merge params into existing url querystring', async () => {
|
|
const params = { a: 1, b: true }
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo?banana=pudding', params },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(`${baseUrl}/echo?banana=pudding&a=1&b=true`)
|
|
})
|
|
|
|
it('should accept a URLSearchParams instance', async () => {
|
|
const params = new URLSearchParams({ apple: 'fruit' })
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', params },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(`${baseUrl}/echo?apple=fruit`)
|
|
})
|
|
|
|
it('should strip undefined values from the end params', async () => {
|
|
const params = { apple: 'fruit', banana: undefined, randy: null }
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', params },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(`${baseUrl}/echo?apple=fruit&randy=null`)
|
|
})
|
|
|
|
it('should support a paramsSerializer', async () => {
|
|
const baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
paramsSerializer: (params: Record<string, unknown>) =>
|
|
queryString.stringify(params, { arrayFormat: 'bracket' }),
|
|
})
|
|
|
|
const api = createApi({
|
|
baseQuery,
|
|
endpoints(build) {
|
|
return {
|
|
query: build.query({
|
|
query: () => ({ url: '/echo', headers: {} }),
|
|
}),
|
|
mutation: build.mutation({
|
|
query: () => ({
|
|
url: '/echo',
|
|
method: 'POST',
|
|
credentials: 'omit',
|
|
}),
|
|
}),
|
|
}
|
|
},
|
|
})
|
|
|
|
const params = {
|
|
someArray: ['a', 'b', 'c'],
|
|
}
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', params },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(
|
|
`${baseUrl}/echo?someArray[]=a&someArray[]=b&someArray[]=c`,
|
|
)
|
|
})
|
|
|
|
it('should supports a custom isJsonContentType function', async () => {
|
|
const testBody = {
|
|
i_should_be_stringified: true,
|
|
}
|
|
const baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
isJsonContentType: (headers) =>
|
|
[
|
|
'application/vnd.api+json',
|
|
'application/json',
|
|
'application/vnd.hal+json',
|
|
].includes(headers.get('content-type') ?? ''),
|
|
})
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
method: 'POST',
|
|
body: testBody,
|
|
headers: { 'content-type': 'application/vnd.hal+json' },
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.body).toMatchObject(testBody)
|
|
})
|
|
})
|
|
|
|
describe('validateStatus', () => {
|
|
test('validateStatus can return an error even on normal 200 responses', async () => {
|
|
// This is a scenario where an API may always return a 200, but indicates there is an error when success = false
|
|
const res = await baseQuery(
|
|
{
|
|
url: '/nonstandard-error',
|
|
validateStatus: (response, body) =>
|
|
response.status === 200 && body.success === false ? false : true,
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toEqual({
|
|
status: 200,
|
|
data: {
|
|
success: false,
|
|
message: 'This returns a 200 but is really an error',
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('arg.headers and prepareHeaders', () => {
|
|
test('uses the default headers set in prepareHeaders', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['fake']).toBe(defaultHeaders['fake'])
|
|
expect(request.headers['delete']).toBe(defaultHeaders['delete'])
|
|
expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
|
|
})
|
|
|
|
test('adds endpoint-level headers to the defaults', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', headers: { authorization: 'Bearer banana' } },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['authorization']).toBe('Bearer banana')
|
|
expect(request.headers['fake']).toBe(defaultHeaders['fake'])
|
|
expect(request.headers['delete']).toBe(defaultHeaders['delete'])
|
|
expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
|
|
})
|
|
|
|
test('it does not set application/json when content-type is set', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
headers: {
|
|
authorization: 'Bearer banana',
|
|
'content-type': 'custom-content-type',
|
|
},
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['authorization']).toBe('Bearer banana')
|
|
expect(request.headers['content-type']).toBe('custom-content-type')
|
|
expect(request.headers['fake']).toBe(defaultHeaders['fake'])
|
|
expect(request.headers['delete']).toBe(defaultHeaders['delete'])
|
|
expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
|
|
})
|
|
|
|
test('respects the headers from an endpoint over the base headers', async () => {
|
|
const fake = 'fake endpoint value'
|
|
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', headers: { fake, delete: '', delete2: '' } },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['fake']).toBe(fake)
|
|
expect(request.headers['delete']).toBeUndefined()
|
|
expect(request.headers['delete2']).toBeUndefined()
|
|
})
|
|
|
|
test('prepareHeaders can return undefined', async () => {
|
|
let request: any
|
|
|
|
const token = 'accessToken'
|
|
|
|
const _baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
prepareHeaders: (headers) => {
|
|
headers.set('authorization', `Bearer ${token}`)
|
|
},
|
|
})
|
|
|
|
const doRequest = async () =>
|
|
_baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
|
|
|
|
;({ data: request } = await doRequest())
|
|
|
|
expect(request.headers['authorization']).toBe(`Bearer ${token}`)
|
|
})
|
|
|
|
test('prepareHeaders is able to be an async function', async () => {
|
|
let request: any
|
|
|
|
const token = 'accessToken'
|
|
const getAccessTokenAsync = async () => token
|
|
|
|
const _baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
prepareHeaders: async (headers) => {
|
|
headers.set('authorization', `Bearer ${await getAccessTokenAsync()}`)
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const doRequest = async () =>
|
|
_baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
|
|
|
|
;({ data: request } = await doRequest())
|
|
|
|
expect(request.headers['authorization']).toBe(`Bearer ${token}`)
|
|
})
|
|
|
|
test('prepareHeaders is able to be an async function returning undefined', async () => {
|
|
let request: any
|
|
|
|
const token = 'accessToken'
|
|
const getAccessTokenAsync = async () => token
|
|
|
|
const _baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
prepareHeaders: async (headers) => {
|
|
headers.set('authorization', `Bearer ${await getAccessTokenAsync()}`)
|
|
},
|
|
})
|
|
|
|
const doRequest = async () =>
|
|
_baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
|
|
|
|
;({ data: request } = await doRequest())
|
|
|
|
expect(request.headers['authorization']).toBe(`Bearer ${token}`)
|
|
})
|
|
|
|
test('prepareHeaders is able to select from a state', async () => {
|
|
let request: any
|
|
|
|
const doRequest = async () => {
|
|
const abortController = new AbortController()
|
|
return baseQuery(
|
|
{ url: '/echo' },
|
|
{
|
|
signal: abortController.signal,
|
|
abort: (reason) =>
|
|
// @ts-ignore
|
|
abortController.abort(reason),
|
|
dispatch: storeRef.store.dispatch,
|
|
getState: storeRef.store.getState,
|
|
extra: undefined,
|
|
type: 'query',
|
|
endpoint: '',
|
|
},
|
|
{},
|
|
)
|
|
}
|
|
|
|
;({ data: request } = await doRequest())
|
|
|
|
expect(request.headers['authorization']).toBeUndefined()
|
|
|
|
// Set a token and the follow up request should have the header injected by prepareHeaders
|
|
const token = 'fakeToken!'
|
|
storeRef.store.dispatch(authSlice.actions.setToken(token))
|
|
;({ data: request } = await doRequest())
|
|
|
|
expect(request.headers['authorization']).toBe(`Bearer ${token}`)
|
|
})
|
|
|
|
test('prepareHeaders provides extra api information for getState, extra, endpoint, type and forced', async () => {
|
|
let _getState, _arg: any, _extra, _endpoint, _type, _forced
|
|
|
|
const baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (
|
|
headers,
|
|
{ getState, arg, extra, endpoint, type, forced },
|
|
) => {
|
|
_getState = getState
|
|
_arg = arg
|
|
_endpoint = endpoint
|
|
_type = type
|
|
_forced = forced
|
|
_extra = extra
|
|
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const fakeAuth0Client = {
|
|
getTokenSilently: async () => 'fakeToken',
|
|
}
|
|
|
|
const doRequest = async () => {
|
|
const abortController = new AbortController()
|
|
return baseQuery(
|
|
{ url: '/echo' },
|
|
{
|
|
signal: abortController.signal,
|
|
abort: (reason) =>
|
|
// @ts-ignore
|
|
abortController.abort(reason),
|
|
dispatch: storeRef.store.dispatch,
|
|
getState: storeRef.store.getState,
|
|
extra: fakeAuth0Client,
|
|
type: 'query',
|
|
forced: true,
|
|
endpoint: 'someEndpointName',
|
|
},
|
|
{},
|
|
)
|
|
}
|
|
|
|
await doRequest()
|
|
|
|
expect(_getState).toBeDefined()
|
|
expect(_arg!.url).toBe('/echo')
|
|
expect(_endpoint).toBe('someEndpointName')
|
|
expect(_type).toBe('query')
|
|
expect(_forced).toBe(true)
|
|
expect(_extra).toBe(fakeAuth0Client)
|
|
})
|
|
|
|
test('can be instantiated with a `ExtraOptions` generic and `extraOptions` will be available in `prepareHeaders', async () => {
|
|
const prepare = vitest.fn()
|
|
const baseQuery = fetchBaseQuery({
|
|
prepareHeaders(headers, api) {
|
|
expectTypeOf(api.extraOptions).toEqualTypeOf<unknown>()
|
|
prepare.apply(undefined, arguments as unknown as any[])
|
|
},
|
|
})
|
|
baseQuery('https://example.com', commonBaseQueryApi, {
|
|
foo: 'baz',
|
|
bar: 5,
|
|
})
|
|
expect(prepare).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ extraOptions: { foo: 'baz', bar: 5 } }),
|
|
)
|
|
|
|
// ensure types
|
|
createApi({
|
|
baseQuery,
|
|
endpoints(build) {
|
|
return {
|
|
testQuery: build.query({
|
|
query: () => ({ url: '/echo', headers: {} }),
|
|
extraOptions: {
|
|
foo: 'asd',
|
|
bar: 1,
|
|
},
|
|
}),
|
|
testMutation: build.mutation({
|
|
query: () => ({
|
|
url: '/echo',
|
|
method: 'POST',
|
|
credentials: 'omit',
|
|
}),
|
|
extraOptions: {
|
|
foo: 'qwe',
|
|
bar: 15,
|
|
},
|
|
}),
|
|
}
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
test('can pass `headers` into `fetchBaseQuery`', async () => {
|
|
let request: any
|
|
|
|
const token = 'accessToken'
|
|
|
|
const _baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
headers: { authorization: `Bearer ${token}` },
|
|
})
|
|
|
|
const doRequest = async () =>
|
|
_baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
|
|
|
|
;({ data: request } = await doRequest())
|
|
|
|
expect(request.headers['authorization']).toBe(`Bearer ${token}`)
|
|
})
|
|
|
|
test('lets a header be undefined', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', headers: undefined },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['fake']).toBe(defaultHeaders['fake'])
|
|
expect(request.headers['delete']).toBe(defaultHeaders['delete'])
|
|
expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
|
|
})
|
|
|
|
test('allows for possibly undefined header key/values', async () => {
|
|
const banana = '1' as '1' | undefined
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', headers: { banana } },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['banana']).toBe('1')
|
|
expect(request.headers['fake']).toBe(defaultHeaders['fake'])
|
|
expect(request.headers['delete']).toBe(defaultHeaders['delete'])
|
|
expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
|
|
})
|
|
|
|
test('strips undefined values from the headers', async () => {
|
|
const banana = undefined as '1' | undefined
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', headers: { banana } },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['banana']).toBeUndefined()
|
|
expect(request.headers['fake']).toBe(defaultHeaders['fake'])
|
|
expect(request.headers['delete']).toBe(defaultHeaders['delete'])
|
|
expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
|
|
})
|
|
|
|
describe('Accepts global arguments', () => {
|
|
test('Global responseHandler', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.text(`this is not json!`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'text',
|
|
})
|
|
|
|
const req = globalizedBaseQuery(
|
|
{ url: '/success' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
const res = await req
|
|
expect(res).toBeInstanceOf(Object)
|
|
expect(res.meta?.request).toBeInstanceOf(Request)
|
|
expect(res.meta?.response).toBeInstanceOf(Object)
|
|
expect(res.error).toBeUndefined()
|
|
expect(res.data).toEqual(`this is not json!`)
|
|
})
|
|
|
|
test('Global responseHandler: content-type with text response', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.text(`this is plain text!`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'content-type',
|
|
})
|
|
|
|
const res = await globalizedBaseQuery(
|
|
{ url: '/success' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toBeUndefined()
|
|
expect(res.data).toEqual(`this is plain text!`)
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'text/plain',
|
|
)
|
|
})
|
|
|
|
test('Global responseHandler: content-type with JSON response', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.json({ message: 'this is json!' }),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'content-type',
|
|
})
|
|
|
|
const res = await globalizedBaseQuery(
|
|
{ url: '/success' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toBeUndefined()
|
|
expect(res.data).toEqual({ message: 'this is json!' })
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'application/json',
|
|
)
|
|
})
|
|
|
|
test('Global responseHandler: content-type can be overridden at endpoint level', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.text(`this is text but will be parsed as json`),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'content-type',
|
|
})
|
|
|
|
// Override global content-type handler with explicit text handler
|
|
const res = await globalizedBaseQuery(
|
|
{ url: '/success', responseHandler: 'text' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toBeUndefined()
|
|
expect(res.data).toEqual(`this is text but will be parsed as json`)
|
|
})
|
|
|
|
test('Global responseHandler: content-type with error response (text)', async () => {
|
|
const errorMessage = 'Internal Server Error'
|
|
server.use(
|
|
http.get('https://example.com/error', () =>
|
|
HttpResponse.text(errorMessage, { status: 500 }),
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'content-type',
|
|
})
|
|
|
|
const res = await globalizedBaseQuery(
|
|
{ url: '/error' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toEqual({
|
|
status: 500,
|
|
data: errorMessage,
|
|
})
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'text/plain',
|
|
)
|
|
})
|
|
|
|
test('Global responseHandler: content-type with error response (JSON)', async () => {
|
|
const errorData = { error: 'Something went wrong', code: 'ERR_500' }
|
|
server.use(
|
|
http.get('https://example.com/error', () =>
|
|
HttpResponse.json(errorData, { status: 500 }),
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'content-type',
|
|
})
|
|
|
|
const res = await globalizedBaseQuery(
|
|
{ url: '/error' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toEqual({
|
|
status: 500,
|
|
data: errorData,
|
|
})
|
|
expect(res.meta?.response?.headers.get('content-type')).toEqual(
|
|
'application/json',
|
|
)
|
|
})
|
|
|
|
test('Global validateStatus', async () => {
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
validateStatus: (response, body) =>
|
|
response.status === 200 && body.success === false ? false : true,
|
|
})
|
|
|
|
// This is a scenario where an API may always return a 200, but indicates there is an error when success = false
|
|
const res = await globalizedBaseQuery(
|
|
{
|
|
url: '/nonstandard-error',
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(res.error).toEqual({
|
|
status: 200,
|
|
data: {
|
|
success: false,
|
|
message: 'This returns a 200 but is really an error',
|
|
},
|
|
})
|
|
})
|
|
|
|
test('Global timeout', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/empty1',
|
|
async ({ request, cookies, params, requestId }) => {
|
|
await delay(300)
|
|
|
|
return HttpResponse.json({
|
|
...request,
|
|
cookies,
|
|
params,
|
|
requestId,
|
|
url: new URL(request.url),
|
|
headers: headersToObject(request.headers),
|
|
})
|
|
},
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const globalizedBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
timeout: 200,
|
|
})
|
|
|
|
const result = await globalizedBaseQuery(
|
|
{ url: '/empty1' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(result?.error).toEqual({
|
|
status: 'TIMEOUT_ERROR',
|
|
error: expect.stringMatching(/^AbortError:/),
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('fetchFn', () => {
|
|
test('accepts a custom fetchFn', async () => {
|
|
const baseUrl = 'https://example.com'
|
|
const params = new URLSearchParams({ apple: 'fruit' })
|
|
|
|
const baseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: nodeFetch as any,
|
|
})
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', params },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.url).toEqual(`${baseUrl}/echo?apple=fruit`)
|
|
})
|
|
|
|
test('respects mocking window.fetch after a fetch base query is created', async () => {
|
|
const baseUrl = 'https://example.com'
|
|
const baseQuery = fetchBaseQuery({ baseUrl })
|
|
|
|
const fakeResponse = {
|
|
ok: true,
|
|
status: 200,
|
|
text: async () => `{ "url": "mock-return-url" }`,
|
|
clone: () => fakeResponse,
|
|
}
|
|
|
|
const spiedFetch = vi.spyOn(window, 'fetch')
|
|
spiedFetch.mockResolvedValueOnce(fakeResponse as any)
|
|
|
|
const { data } = await baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
|
|
expect(data).toEqual({ url: 'mock-return-url' })
|
|
|
|
spiedFetch.mockClear()
|
|
})
|
|
})
|
|
|
|
describe('FormData', () => {
|
|
test('sets the right headers when sending FormData', async () => {
|
|
const body = new FormData()
|
|
|
|
body.append('username', 'test')
|
|
|
|
body.append(
|
|
'file',
|
|
new Blob([JSON.stringify({ hello: 'there' }, null, 2)], {
|
|
type: 'application/json',
|
|
}),
|
|
)
|
|
|
|
const res = await baseQuery(
|
|
{ url: '/echo', method: 'POST', body },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
const request: any = res.data
|
|
|
|
expect(request.headers['content-type']).not.toContain('application/json')
|
|
})
|
|
|
|
test('FormData works correctly when prepareHeaders sets Content-Type to application/json', async () => {
|
|
// This test covers the exact scenario from issue #4669
|
|
const baseQueryWithJsonDefault = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (headers) => {
|
|
// Set default Content-Type for all requests
|
|
headers.set('Content-Type', 'application/json')
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const body = new FormData()
|
|
body.append('username', 'test')
|
|
body.append(
|
|
'file',
|
|
new Blob([JSON.stringify({ hello: 'there' }, null, 2)], {
|
|
type: 'application/json',
|
|
}),
|
|
)
|
|
|
|
const res = await baseQueryWithJsonDefault(
|
|
{ url: '/echo', method: 'POST', body },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
const request: any = res.data
|
|
|
|
// The Content-Type should NOT be application/json when FormData is used
|
|
expect(request.headers['content-type']).not.toContain('application/json')
|
|
// It should contain multipart/form-data (set automatically by the browser)
|
|
expect(request.headers['content-type']).toContain('multipart/form-data')
|
|
})
|
|
|
|
test('FormData works when prepareHeaders conditionally removes Content-Type', async () => {
|
|
// This tests the workaround solution from the issue comments
|
|
const baseQueryWithConditionalHeader = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (headers, { arg }) => {
|
|
// Check if body is FormData and skip setting Content-Type
|
|
if ((arg as FetchArgs).body instanceof FormData) {
|
|
// Delete Content-Type to let browser set it automatically
|
|
headers.delete('Content-Type')
|
|
} else {
|
|
// Set default Content-Type for non-FormData requests
|
|
headers.set('Content-Type', 'application/json')
|
|
}
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const body = new FormData()
|
|
body.append('username', 'test')
|
|
body.append('file', new Blob(['test content'], { type: 'text/plain' }))
|
|
|
|
const res = await baseQueryWithConditionalHeader(
|
|
{ url: '/echo', method: 'POST', body },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
const request: any = res.data
|
|
|
|
// Should have multipart/form-data set by browser
|
|
expect(request.headers['content-type']).toContain('multipart/form-data')
|
|
expect(request.headers['content-type']).not.toContain('application/json')
|
|
})
|
|
|
|
test('endpoint-level headers cannot override to multipart/form-data manually', async () => {
|
|
// This tests the fetch API quirk mentioned in the issue
|
|
const baseQueryWithJsonDefault = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (headers) => {
|
|
headers.set('Content-Type', 'application/json')
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const body = new FormData()
|
|
body.append('test', 'value')
|
|
|
|
const res = await baseQueryWithJsonDefault(
|
|
{
|
|
url: '/echo',
|
|
method: 'POST',
|
|
body,
|
|
// Attempting to manually set multipart/form-data (this won't work as expected)
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
const request: any = res.data
|
|
|
|
// Due to prepareHeaders running after endpoint headers,
|
|
// and the fetch API not allowing manual multipart/form-data setting,
|
|
// this demonstrates the problem from the issue
|
|
// The actual behavior depends on fetchBaseQuery implementation
|
|
expect(request.headers['content-type']).toBeDefined()
|
|
})
|
|
|
|
test('non-FormData requests still get application/json from prepareHeaders', async () => {
|
|
// Verify that the workaround doesn't break normal JSON requests
|
|
const baseQueryWithConditionalHeader = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (headers, { arg }) => {
|
|
if (!((arg as FetchArgs).body instanceof FormData)) {
|
|
headers.set('Content-Type', 'application/json')
|
|
}
|
|
return headers
|
|
},
|
|
})
|
|
|
|
const jsonBody = { test: 'value' }
|
|
|
|
const res = await baseQueryWithConditionalHeader(
|
|
{ url: '/echo', method: 'POST', body: jsonBody },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
const request: any = res.data
|
|
|
|
// Regular JSON requests should still get application/json
|
|
expect(request.headers['content-type']).toBe('application/json')
|
|
expect(request.body).toEqual(jsonBody)
|
|
})
|
|
})
|
|
|
|
describe('Accept header handling', () => {
|
|
test('sets Accept header to application/json for json responseHandler', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', responseHandler: 'json' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['accept']).toBe('application/json')
|
|
})
|
|
|
|
test('sets Accept header to application/json by default (json is default responseHandler)', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['accept']).toBe('application/json')
|
|
})
|
|
|
|
test('sets Accept header for text responseHandler', async () => {
|
|
// Create a baseQuery with text as the global responseHandler
|
|
const textBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'text',
|
|
})
|
|
|
|
let request: any
|
|
// Override to json just for this test so we can inspect the echoed request object
|
|
;({ data: request } = await textBaseQuery(
|
|
{ url: '/echo', responseHandler: 'json' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
// The endpoint-level 'json' responseHandler overrides the global 'text',
|
|
// so the Accept header should be application/json
|
|
expect(request.headers['accept']).toBe('application/json')
|
|
})
|
|
|
|
test('does not override explicit Accept header from endpoint', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{
|
|
url: '/echo',
|
|
responseHandler: 'json',
|
|
headers: { Accept: 'application/xml' },
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['accept']).toBe('application/xml')
|
|
})
|
|
|
|
test('does not override Accept header set in prepareHeaders', async () => {
|
|
const customBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
prepareHeaders: (headers) => {
|
|
headers.set('Accept', 'application/vnd.api+json')
|
|
return headers
|
|
},
|
|
})
|
|
|
|
let request: any
|
|
;({ data: request } = await customBaseQuery(
|
|
{ url: '/echo', responseHandler: 'json' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
expect(request.headers['accept']).toBe('application/vnd.api+json')
|
|
})
|
|
|
|
test('does not set Accept header for content-type responseHandler', async () => {
|
|
let request: any
|
|
;({ data: request } = await baseQuery(
|
|
{ url: '/echo', responseHandler: 'content-type' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
// Should either not have accept header or have a permissive one
|
|
// content-type handler adapts to whatever server sends
|
|
const acceptHeader = request.headers['accept']
|
|
if (acceptHeader) {
|
|
expect(acceptHeader).toMatch(/\*\/\*/)
|
|
}
|
|
})
|
|
|
|
test('respects global responseHandler for Accept header', async () => {
|
|
const textBaseQuery = fetchBaseQuery({
|
|
baseUrl,
|
|
fetchFn: fetchFn as any,
|
|
responseHandler: 'text',
|
|
})
|
|
|
|
let request: any
|
|
// Override to json just for this test so we can inspect the echoed request object
|
|
;({ data: request } = await textBaseQuery(
|
|
{ url: '/echo', responseHandler: 'json' },
|
|
commonBaseQueryApi,
|
|
{},
|
|
))
|
|
|
|
// The endpoint-level 'json' responseHandler overrides the global 'text',
|
|
// so the Accept header should be application/json (proving endpoint-level takes precedence)
|
|
expect(request.headers['accept']).toBe('application/json')
|
|
})
|
|
})
|
|
|
|
describe('still throws on completely unexpected errors', () => {
|
|
test('', async () => {
|
|
const error = new Error('some unexpected error')
|
|
const req = baseQuery(
|
|
{
|
|
url: '/success',
|
|
validateStatus() {
|
|
throw error
|
|
},
|
|
},
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
expect(req).toBeInstanceOf(Promise)
|
|
await expect(req).rejects.toBe(error)
|
|
})
|
|
})
|
|
|
|
describe('timeout', () => {
|
|
test('throws a timeout error when a request takes longer than specified timeout duration', async () => {
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/empty2',
|
|
async ({ request, cookies, params, requestId }) => {
|
|
await delay(300)
|
|
|
|
return HttpResponse.json({
|
|
...request,
|
|
url: new URL(request.url),
|
|
cookies,
|
|
params,
|
|
requestId,
|
|
headers: headersToObject(request.headers),
|
|
})
|
|
},
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const result = await baseQuery(
|
|
{ url: '/empty2', timeout: 200 },
|
|
commonBaseQueryApi,
|
|
{},
|
|
)
|
|
|
|
expect(result?.error).toEqual({
|
|
status: 'TIMEOUT_ERROR',
|
|
error: expect.stringMatching(/^AbortError:/),
|
|
})
|
|
})
|
|
})
|