52 - Add application/json auth call path to api client
This commit is contained in:
parent
20c41c7a65
commit
244f9ec314
2 changed files with 65 additions and 1 deletions
38
frontend/src/lib/api.test.ts
Normal file
38
frontend/src/lib/api.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { server } from '../test/server'
|
||||||
|
import { ApiError, api } from './api'
|
||||||
|
|
||||||
|
describe('api.postJson', () => {
|
||||||
|
it('sends application/json and returns the parsed body', async () => {
|
||||||
|
let sentContentType: string | null = null
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.post('http://localhost/api/login', async ({ request }) => {
|
||||||
|
sentContentType = request.headers.get('Content-Type')
|
||||||
|
return HttpResponse.json({ id: 'u1', email: 'a@b.com' })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await api.postJson<{ id: string; email: string }>('/login', {
|
||||||
|
username: 'a@b.com',
|
||||||
|
password: 'secret',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(sentContentType).toBe('application/json')
|
||||||
|
expect(result).toEqual({ id: 'u1', email: 'a@b.com' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws ApiError on a non-2xx response', async () => {
|
||||||
|
server.use(
|
||||||
|
http.post('http://localhost/api/login', () =>
|
||||||
|
HttpResponse.json({ message: 'bad creds' }, { status: 401 }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const error = await api.postJson('/login', {}).catch((e: unknown) => e)
|
||||||
|
|
||||||
|
expect(error).toBeInstanceOf(ApiError)
|
||||||
|
expect((error as ApiError).status).toBe(401)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -4,7 +4,11 @@
|
||||||
* Auth is cookie-session (#30): every request sends credentials so the session
|
* Auth is cookie-session (#30): every request sends credentials so the session
|
||||||
* cookie set by POST /api/login flows on subsequent calls. In dev the Vite proxy
|
* cookie set by POST /api/login flows on subsequent calls. In dev the Vite proxy
|
||||||
* keeps /api same-origin, so no CORS is involved. JSON-LD (Hydra) is the wire
|
* keeps /api same-origin, so no CORS is involved. JSON-LD (Hydra) is the wire
|
||||||
* format; PATCH uses application/merge-patch+json per API Platform.
|
* format for API Platform resources; PATCH uses application/merge-patch+json.
|
||||||
|
*
|
||||||
|
* The auth endpoints (login/register/logout) are NOT API Platform resources —
|
||||||
|
* json_login expects application/json and rejects ld+json — so they use the
|
||||||
|
* `postJson` path (plain application/json) instead of `post`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const BASE = '/api'
|
const BASE = '/api'
|
||||||
|
|
@ -23,6 +27,14 @@ export class ApiError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
|
// Paths are relative to BASE — callers pass `/login`, not `/api/login`.
|
||||||
|
// Guard against accidentally double-prefixing (a real footgun in tests).
|
||||||
|
if (path.startsWith(BASE)) {
|
||||||
|
throw new Error(
|
||||||
|
`API path "${path}" must not include the "${BASE}" prefix — it is added automatically.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`${BASE}${path}`, {
|
const res = await fetch(`${BASE}${path}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
...init,
|
...init,
|
||||||
|
|
@ -56,6 +68,20 @@ export const api = {
|
||||||
headers: { 'Content-Type': 'application/ld+json' },
|
headers: { 'Content-Type': 'application/ld+json' },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST with plain application/json — for the auth endpoints (login, register,
|
||||||
|
* logout) which are not API Platform resources and reject ld+json.
|
||||||
|
*/
|
||||||
|
postJson: <T>(path: string, body: unknown): Promise<T> =>
|
||||||
|
request<T>(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
patch: <T>(path: string, body: unknown): Promise<T> =>
|
patch: <T>(path: string, body: unknown): Promise<T> =>
|
||||||
request<T>(path, {
|
request<T>(path, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue