/ src / components / admin / __tests__ / AdminGuard.test.tsx
AdminGuard.test.tsx
  1  // Copyright (c) 2026 VPL Solutions. All rights reserved.
  2  // Licensed under the MIT License. See LICENSE for details.
  3  
  4  import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
  5  import { render, screen, waitFor, cleanup } from '@testing-library/react';
  6  import { http, HttpResponse } from 'msw';
  7  import { setupServer } from 'msw/node';
  8  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  9  import { AdminGuard } from '../AdminGuard';
 10  import { AuthContext, type AuthContextValue } from '../../../auth/AuthContext';
 11  
 12  const server = setupServer();
 13  
 14  beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
 15  afterEach(() => { cleanup(); server.resetHandlers(); });
 16  afterAll(() => server.close());
 17  
 18  function makeAuth(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
 19    return {
 20      isAuthenticated: true,
 21      authEnabled: true,
 22      user: null,
 23      roles: [],
 24      getAccessToken: async () => null,
 25      login: async () => {},
 26      logout: async () => {},
 27      ...overrides,
 28    };
 29  }
 30  
 31  function renderGuard(auth: AuthContextValue) {
 32    const queryClient = new QueryClient({
 33      defaultOptions: { queries: { retry: false } },
 34    });
 35    return render(
 36      <AuthContext.Provider value={auth}>
 37        <QueryClientProvider client={queryClient}>
 38          <AdminGuard>
 39            <div data-testid="protected-content">Admin Content</div>
 40          </AdminGuard>
 41        </QueryClientProvider>
 42      </AuthContext.Provider>,
 43    );
 44  }
 45  
 46  describe('AdminGuard', () => {
 47    it('renders children when auth is disabled', () => {
 48      renderGuard(makeAuth({ authEnabled: false }));
 49      expect(screen.getByTestId('protected-content')).toBeInTheDocument();
 50    });
 51  
 52    it('renders children when whoami returns is_admin: true', async () => {
 53      server.use(
 54        http.get('http://localhost:8000/admin/roles/whoami', () =>
 55          HttpResponse.json({ is_admin: true, email: 'admin@test.com', roles: ['operator'] }),
 56        ),
 57      );
 58      renderGuard(makeAuth());
 59      await waitFor(() => {
 60        expect(screen.getByTestId('protected-content')).toBeInTheDocument();
 61      });
 62    });
 63  
 64    it('shows access restricted when whoami returns is_admin: false', async () => {
 65      server.use(
 66        http.get('http://localhost:8000/admin/roles/whoami', () =>
 67          HttpResponse.json({ is_admin: false }),
 68        ),
 69      );
 70      renderGuard(makeAuth());
 71      await waitFor(() => {
 72        expect(screen.getByText('Access Restricted')).toBeInTheDocument();
 73      });
 74      expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
 75    });
 76  
 77    it('shows access restricted when whoami returns 403', async () => {
 78      server.use(
 79        http.get('http://localhost:8000/admin/roles/whoami', () =>
 80          HttpResponse.json({ detail: 'Forbidden' }, { status: 403 }),
 81        ),
 82      );
 83      renderGuard(makeAuth());
 84      await waitFor(() => {
 85        expect(screen.getByText('Access Restricted')).toBeInTheDocument();
 86      });
 87    });
 88  
 89    it('shows loading spinner while whoami is pending', () => {
 90      server.use(
 91        http.get('http://localhost:8000/admin/roles/whoami', () =>
 92          new Promise(() => {}), // never resolves
 93        ),
 94      );
 95      renderGuard(makeAuth());
 96      expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
 97      expect(screen.queryByText('Access Restricted')).not.toBeInTheDocument();
 98    });
 99  
100    it('shows session-not-ready message (not Access Restricted) when whoami returns 401', async () => {
101      server.use(
102        http.get('http://localhost:8000/admin/roles/whoami', () =>
103          HttpResponse.json({ detail: 'Missing Bearer token' }, { status: 401 }),
104        ),
105      );
106      renderGuard(makeAuth());
107      await waitFor(() => {
108        expect(screen.getByText('Session not ready')).toBeInTheDocument();
109      }, { timeout: 6000 }); // 2 retries × 1500ms retryDelay = 3000ms minimum
110      // Must NOT show "Access Restricted" for auth errors
111      expect(screen.queryByText('Access Restricted')).not.toBeInTheDocument();
112      expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
113      // Retry button is present
114      expect(screen.getByRole('button', { name: /Retry/i })).toBeInTheDocument();
115    });
116  });