diff --git a/frontend/src/auth/AuthProvider.test.tsx b/frontend/src/auth/AuthProvider.test.tsx
new file mode 100644
index 0000000..5d6490a
--- /dev/null
+++ b/frontend/src/auth/AuthProvider.test.tsx
@@ -0,0 +1,94 @@
+import { act, render, screen } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { fakeApi, json, problem, session } from '../test/fakeApi.ts';
+import { AuthProvider } from './AuthProvider.tsx';
+import { type Auth, useAuth } from './context.ts';
+
+/** Shows the state and hands out the context, so that logout() can be called and its promise observed. */
+function renderProvider(): { auth: () => Auth } {
+ let current: Auth | null = null;
+ function Probe() {
+ current = useAuth();
+ const { state } = current;
+ return
{state.status === 'anonymous' ? `anonymous ended=${String(state.ended)}` : state.status}
;
+ }
+ render(
+
+
+ ,
+ );
+ return {
+ auth: () => {
+ if (current === null) {
+ throw new Error('not rendered');
+ }
+ return current;
+ },
+ };
+}
+
+/*
+ * The AuthProvider's own logout, without the AppShell: the global 401
+ * listener also ends the state on a 401, so App-level tests pass whether
+ * logout() resolves or rejects there. These tests pin the promise itself.
+ */
+describe('AuthProvider logout', () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it('resolves on a 401, as the session has already ended on the server, and is signed out', async () => {
+ fakeApi({
+ 'GET /api/v1/auth/session': json(200, session()),
+ 'POST /api/v1/auth/logout': problem(401, 'unauthenticated'),
+ });
+ const { auth } = renderProvider();
+ expect(await screen.findByText('authenticated')).toBeInTheDocument();
+
+ let outcome: Promise = Promise.resolve();
+ await act(async () => {
+ outcome = auth().logout();
+ await outcome.catch(() => undefined);
+ });
+
+ await expect(outcome).resolves.toBeUndefined();
+ // Signed out by the user, not "the session has ended".
+ expect(screen.getByTestId('state')).toHaveTextContent('anonymous ended=false');
+ });
+
+ it('resolves on a 204 and is signed out', async () => {
+ fakeApi({
+ 'GET /api/v1/auth/session': json(200, session()),
+ 'POST /api/v1/auth/logout': new Response(null, { status: 204 }),
+ });
+ const { auth } = renderProvider();
+ expect(await screen.findByText('authenticated')).toBeInTheDocument();
+
+ let outcome: Promise = Promise.resolve();
+ await act(async () => {
+ outcome = auth().logout();
+ await outcome.catch(() => undefined);
+ });
+
+ await expect(outcome).resolves.toBeUndefined();
+ expect(screen.getByTestId('state')).toHaveTextContent('anonymous ended=false');
+ });
+
+ it('rejects on a 500 and stays signed in, as the session may still be valid', async () => {
+ fakeApi({
+ 'GET /api/v1/auth/session': json(200, session()),
+ 'POST /api/v1/auth/logout': problem(500, 'internal-error'),
+ });
+ const { auth } = renderProvider();
+ expect(await screen.findByText('authenticated')).toBeInTheDocument();
+
+ let outcome: Promise = Promise.resolve();
+ await act(async () => {
+ outcome = auth().logout();
+ await outcome.catch(() => undefined);
+ });
+
+ await expect(outcome).rejects.toMatchObject({ status: 500 });
+ expect(screen.getByTestId('state')).toHaveTextContent('authenticated');
+ });
+});