Reviewed-on: #433 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
@@ -28,11 +28,17 @@ vi.mock ('@/config', () => ({
|
||||
|
||||
describe ('api helpers', () => {
|
||||
beforeEach (() => {
|
||||
vi.resetModules ()
|
||||
vi.clearAllMocks ()
|
||||
localStorage.clear ()
|
||||
mocks.client.get.mockReset ().mockResolvedValue ({ data: { token: 'csrf-token' } })
|
||||
mocks.client.post.mockReset ().mockResolvedValue ({ data: { ok: true } })
|
||||
mocks.client.patch.mockReset ().mockResolvedValue ({ data: { ok: true } })
|
||||
mocks.client.delete.mockReset ().mockResolvedValue ({ data: '' })
|
||||
mocks.isAxiosError.mockReturnValue (false)
|
||||
})
|
||||
|
||||
it ('adds the transfer code header and camelizes get responses', async () => {
|
||||
it ('uses Cookies without sending the recovery code and camelises responses', async () => {
|
||||
localStorage.setItem ('user_code', 'abc123')
|
||||
mocks.client.get.mockResolvedValueOnce ({
|
||||
data: { post_id: 1, nested_value: { created_at: 'now' } },
|
||||
@@ -47,11 +53,13 @@ describe ('api helpers', () => {
|
||||
expect (mocks.client.get).toHaveBeenCalledWith (
|
||||
'/posts/1',
|
||||
{
|
||||
headers: { 'X-Transfer-Code': 'abc123', 'X-Extra': '1' },
|
||||
headers: { 'X-Extra': '1' },
|
||||
params: { page: 2 },
|
||||
},
|
||||
)
|
||||
expect (data).toEqual ({ postId: 1, nestedValue: { createdAt: 'now' } })
|
||||
const { default: axios } = await import ('axios')
|
||||
expect (axios.create).toHaveBeenCalledWith ({ baseURL: '/api', withCredentials: true })
|
||||
})
|
||||
|
||||
it ('passes an empty body for post-like requests when body is omitted', async () => {
|
||||
@@ -63,7 +71,7 @@ describe ('api helpers', () => {
|
||||
expect (mocks.client.patch).toHaveBeenCalledWith (
|
||||
'/posts/1',
|
||||
{},
|
||||
{ headers: { 'X-Transfer-Code': '' } },
|
||||
{ headers: { 'X-CSRF-Token': 'csrf-token' } },
|
||||
)
|
||||
expect (data.okValue).toBe (true)
|
||||
})
|
||||
@@ -88,9 +96,70 @@ describe ('api helpers', () => {
|
||||
|
||||
expect (mocks.client.delete).toHaveBeenCalledWith (
|
||||
'/posts/1',
|
||||
{ headers: { 'X-Transfer-Code': '' } },
|
||||
{ headers: { 'X-CSRF-Token': 'csrf-token' } },
|
||||
)
|
||||
expect (isApiError (err)).toBe (true)
|
||||
expect (mocks.isAxiosError).toHaveBeenCalledWith (err)
|
||||
})
|
||||
|
||||
it ('shares initial CSRF acquisition across concurrent writes and reuses the token', async () => {
|
||||
let complete!: (value: { data: { token: string } }) => void
|
||||
mocks.client.get.mockReturnValueOnce (new Promise (resolve => { complete = resolve }))
|
||||
const { apiPost, apiPatch } = await import ('@/lib/api')
|
||||
const first = apiPost ('/posts', { title: 'one' })
|
||||
const second = apiPatch ('/posts/1', { title: 'two' })
|
||||
expect (mocks.client.get).toHaveBeenCalledTimes (1)
|
||||
expect (mocks.client.post).not.toHaveBeenCalled ()
|
||||
complete ({ data: { token: 'shared-token' } })
|
||||
await Promise.all ([first, second])
|
||||
await apiPatch ('/posts/1')
|
||||
expect (mocks.client.get).toHaveBeenCalledTimes (1)
|
||||
expect (mocks.client.post).toHaveBeenCalledWith ('/posts', { title: 'one' }, {
|
||||
headers: { 'X-CSRF-Token': 'shared-token' } })
|
||||
expect (mocks.client.patch).toHaveBeenLastCalledWith ('/posts/1', { }, {
|
||||
headers: { 'X-CSRF-Token': 'shared-token' } })
|
||||
})
|
||||
|
||||
it.each (['/users', '/users/verify', '/users/code/renew']) (
|
||||
'refreshes CSRF after a session change through %s', async path => {
|
||||
mocks.client.get.mockResolvedValueOnce ({ data: { token: 'before' } })
|
||||
mocks.client.get.mockResolvedValueOnce ({ data: { token: 'after' } })
|
||||
mocks.client.post.mockResolvedValueOnce ({ data: { valid: true } })
|
||||
const { apiPost, apiPatch } = await import ('@/lib/api')
|
||||
await apiPost (path)
|
||||
await apiPatch ('/posts/1')
|
||||
expect (mocks.client.get).toHaveBeenCalledTimes (2)
|
||||
expect (mocks.client.patch).toHaveBeenCalledWith ('/posts/1', { }, {
|
||||
headers: { 'X-CSRF-Token': 'after' } })
|
||||
})
|
||||
|
||||
it ('clears the recovery code and CSRF token after session deletion', async () => {
|
||||
localStorage.setItem ('user_code', 'backup')
|
||||
const { apiDelete, apiPatch } = await import ('@/lib/api')
|
||||
await apiDelete ('/users/session')
|
||||
expect (localStorage.getItem ('user_code')).toBeNull ()
|
||||
await apiPatch ('/posts/1')
|
||||
expect (mocks.client.get).toHaveBeenCalledTimes (2)
|
||||
})
|
||||
|
||||
it ('clears rejected CSRF tokens without replaying a write', async () => {
|
||||
const error = { response: { status: 403, headers: { 'x-csrf-invalid': '1' } } }
|
||||
mocks.isAxiosError.mockReturnValue (true)
|
||||
mocks.client.patch.mockRejectedValueOnce (error)
|
||||
const { apiPatch } = await import ('@/lib/api')
|
||||
await expect (apiPatch ('/posts/1')).rejects.toBe (error)
|
||||
expect (mocks.client.patch).toHaveBeenCalledTimes (1)
|
||||
await apiPatch ('/posts/1')
|
||||
expect (mocks.client.get).toHaveBeenCalledTimes (2)
|
||||
})
|
||||
|
||||
it ('allows CSRF acquisition to recover after a failed request', async () => {
|
||||
const error = new Error ('offline')
|
||||
mocks.client.get.mockRejectedValueOnce (error)
|
||||
const { apiPatch } = await import ('@/lib/api')
|
||||
await expect (apiPatch ('/posts/1')).rejects.toBe (error)
|
||||
expect (mocks.client.patch).not.toHaveBeenCalled ()
|
||||
await apiPatch ('/posts/1')
|
||||
expect (mocks.client.get).toHaveBeenCalledTimes (2)
|
||||
})
|
||||
})
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする