mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:11:10 +00:00
[AZ-510] Auth bootstrap: POST refresh + chained /users/me
Replace the broken `GET /api/admin/auth/refresh` (no `credentials:'include'`) mount-time bootstrap with `POST /api/admin/auth/refresh` (with credentials) chained to `GET /api/admin/users/me`. Returning users with a valid HttpOnly refresh cookie no longer flash through `/login`. Closes Finding B3 / Vision P3. - Add module-scoped `bootstrapInflight` guard (StrictMode double-mount safety) + test-only reset hook exported via the `src/auth` barrel; `tests/setup.ts` resets it in `afterEach` to prevent pending-promise leakage between tests. - Defensive `hasPermission` against legacy `/users/me` payloads omitting `permissions`; default MSW handler now seeds `permissions` explicitly. - Add `endpoints.admin.usersMe()` builder (STC-ARCH-02 forbids the literal). - Bulk-swap 15 test files from `http.get` -> `http.post` for the refresh override so intentional bootstrap-fail tests still fail correctly. - Update auth component description; mark B3 closed. - Code review verdict PASS; static + fast suites green (231 / 13 skipped). Batch report: _docs/03_implementation/batch_13_cycle3_report.md Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -100,7 +100,7 @@ function captureSavePost(): { saves: CapturedSave[] } {
|
||||
}),
|
||||
http.get('/api/annotations/classes', () => jsonResponse([])),
|
||||
http.get('/api/annotations/dataset/info', () => jsonResponse({ totalCount: 1, statusCounts: {} })),
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
)
|
||||
return { saves }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { FlightProvider, Header } from '../src/components'
|
||||
|
||||
function rigHeaderEnv(): void {
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
|
||||
http.get('/api/annotations/settings/user', () => new Response(null, { status: 404 })),
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ function rigDatasetAndBulk(): SyncRig {
|
||||
const validatedAfterPost = { current: false }
|
||||
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
|
||||
http.get('/api/annotations/settings/user', () => new Response(null, { status: 404 })),
|
||||
http.put('/api/annotations/settings/user', () => new Response(null, { status: 200 })),
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('AZ-471 — CanvasEditor (draw / resize / multi-select / zoom / pan)',
|
||||
// an unhandled request triggers MSW's onUnhandledRequest:'error'. A 401
|
||||
// here keeps AuthProvider's `.catch` quiet (loading flips to false) and
|
||||
// satisfies AC-3 of AZ-456.
|
||||
server.use(http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })))
|
||||
server.use(http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })))
|
||||
|
||||
// Force the container's clientWidth/Height (jsdom default = 0) so the
|
||||
// CanvasEditor's `useEffect(isVideo)` populates `imgSize` to 640×480.
|
||||
|
||||
@@ -60,7 +60,7 @@ function captureClassDelete(): { deletes: CapturedDelete[] } {
|
||||
// AuthContext bootstraps with GET /api/admin/auth/refresh; tests using
|
||||
// <ProtectedRoute>-less render still mount AuthProvider. Return 401 so
|
||||
// the unauth path resolves quickly and bootstrap finishes.
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
)
|
||||
return { deletes }
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ function captureClassesGets(payload: DetectionClass[], opts?: { status?: number
|
||||
// unhandled-request errors without affecting these tests (AuthProvider's
|
||||
// .catch swallows the failure and DetectionClasses doesn't depend on auth
|
||||
// user state).
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
)
|
||||
return calls
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ function captureDetectAndBootstrap(opts?: {
|
||||
|
||||
// Bootstrap — minimal handlers so <AnnotationsPage> mounts cleanly and
|
||||
// <MediaList> shows the seeded media item.
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
|
||||
http.get('/api/annotations/settings/user', () => new Response(null, { status: 404 })),
|
||||
http.put('/api/annotations/settings/user', () => new Response(null, { status: 200 })),
|
||||
|
||||
@@ -40,7 +40,7 @@ function rigFlightEnv(opts?: { seedSelectedFlightId?: string | null }): FlightRi
|
||||
|
||||
server.use(
|
||||
// AuthProvider GET — silence MSW unhandled warnings.
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
|
||||
http.get('/api/flights', () => jsonResponse(paginate(seedFlights, 1, 1000))),
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ function captureSettingsPut(): { puts: CapturedPut[] } {
|
||||
}),
|
||||
),
|
||||
http.get('/api/flights/aircrafts', () => jsonResponse([])),
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
)
|
||||
return { puts }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { http } from 'msw'
|
||||
import { jsonResponse, noContent, paginate } from '../helpers'
|
||||
import { seedUsers, opAlice } from '../../fixtures/seed_users'
|
||||
import { seedUsers, opAlice, seedPermissions } from '../../fixtures/seed_users'
|
||||
import { seedClasses } from '../../fixtures/seed_classes'
|
||||
|
||||
// Default `/api/admin/*` handlers — auth round-trip, users, classes-write,
|
||||
@@ -28,7 +28,13 @@ export const adminHandlers = [
|
||||
|
||||
http.post('/api/admin/auth/logout', () => noContent()),
|
||||
|
||||
http.get('/api/admin/users/me', () => jsonResponse(opAlice)),
|
||||
// AZ-510 chains GET /users/me after POST refresh during AuthProvider
|
||||
// bootstrap. The default user shape includes `permissions` so production
|
||||
// code paths (e.g., hasPermission, RBAC route gates) get a realistic
|
||||
// payload without each test having to override.
|
||||
http.get('/api/admin/users/me', () =>
|
||||
jsonResponse({ ...opAlice, permissions: seedPermissions[opAlice.id] ?? [] }),
|
||||
),
|
||||
|
||||
http.get('/api/admin/users', () => jsonResponse(paginate(seedUsers))),
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ const seedAnnotation: AnnotationListItem = {
|
||||
|
||||
function rigDownloadEnv() {
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate(seedFlights, 1, 1000))),
|
||||
http.get('/api/flights/:id', ({ params }) => {
|
||||
const f = seedFlights.find((x) => x.id === params.id)
|
||||
@@ -516,7 +516,7 @@ describe('AZ-478 — AC-3 (NFT-RES-10): SSE disconnect surfaces a connection-los
|
||||
beforeEach(() => {
|
||||
restoreEs = installFakeEventSource()
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ function rigPanelEnv(opts?: { seedSettings?: boolean }): { puts: CapturedPut[] }
|
||||
const puts: CapturedPut[] = []
|
||||
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
|
||||
// The user settings GET — when seedSettings is true, return a payload
|
||||
// that includes both the legacy per-page width fields AND a `panelWidths`
|
||||
|
||||
@@ -103,7 +103,7 @@ function makeHarnessState(): HarnessState {
|
||||
|
||||
function captureClassesGet(payload: DetectionClass[]) {
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/annotations/classes', () => jsonResponse(payload)),
|
||||
)
|
||||
}
|
||||
@@ -262,7 +262,7 @@ interface PostCapture {
|
||||
function rigSaveEnv(): PostCapture {
|
||||
const classNums: number[][] = []
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate(seedFlights, 1, 1000))),
|
||||
http.get('/api/flights/:id', ({ params }) => {
|
||||
const f = seedFlights.find((x) => x.id === params.id)
|
||||
|
||||
@@ -66,7 +66,7 @@ function rigSettingsEnv(failure: SettingsFailure): SettingsRig {
|
||||
const responseAt = { value: null as number | null }
|
||||
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/annotations/settings/system', () => jsonResponse(SYSTEM_SEED)),
|
||||
http.get('/api/annotations/settings/directories', () => jsonResponse(DIRS_SEED)),
|
||||
http.get('/api/flights/aircrafts', () => jsonResponse(seedAircraft)),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { afterAll, afterEach, beforeAll } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { server } from './msw/server'
|
||||
import { setToken, setNavigateToLogin } from '../src/api'
|
||||
import { __resetBootstrapInflightForTests } from '../src/auth'
|
||||
|
||||
// JSDOM polyfills for browser APIs production code touches at mount time.
|
||||
// These are no-op stubs — tests that exercise the actual behavior install
|
||||
@@ -57,6 +58,9 @@ afterEach(() => {
|
||||
/* default no-op for tests; production accessor restored implicitly
|
||||
on next module reload — tests must re-seed if they assert on it. */
|
||||
})
|
||||
// AZ-510 — clear AuthProvider's module-scoped in-flight bootstrap promise so
|
||||
// a never-resolving fixture in test N does not leak into test N+1.
|
||||
__resetBootstrapInflightForTests()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -117,7 +117,7 @@ function datasetRowFromAnnotation(a: AnnotationListItem): DatasetItem {
|
||||
beforeEach(() => {
|
||||
seedBearer()
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
// FlightProvider mounts a user-settings fetch when authenticated. The
|
||||
// dataset surface does not depend on it; we satisfy MSW's unhandled-
|
||||
// request gate with a 404 so the noise does not pollute the report.
|
||||
|
||||
@@ -37,7 +37,7 @@ function rigUploadEnv(opts: { uploadStatus: number }): UploadRig {
|
||||
const posts: { url: string; pathname: string; status: number }[] = []
|
||||
|
||||
server.use(
|
||||
http.get('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
||||
http.get('/api/flights', () => jsonResponse(paginate(seedFlights, 1, 1000))),
|
||||
http.get('/api/flights/:id', ({ params }) => {
|
||||
const f = seedFlights.find((x) => x.id === params.id)
|
||||
|
||||
Reference in New Issue
Block a user