useStreamTokenSync3025.test.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * Tests for the two-token split in useStreamTokenSync (#3025).
  3. *
  4. * Thumbnails, plate previews, timelapses, cover images and link icons used to
  5. * ride on the camera stream token, so a user without camera:view saw broken
  6. * images everywhere and got a 403 from the camera mint on every page load.
  7. * The hook now fetches a media token for everyone and asks for a camera token
  8. * only when the user can actually have one.
  9. */
  10. import type { ReactNode } from 'react';
  11. import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
  12. import { renderHook, waitFor, cleanup } from '@testing-library/react';
  13. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  14. import { http, HttpResponse } from 'msw';
  15. import { server } from '../mocks/server';
  16. import { useStreamTokenSync } from '../../hooks/useCameraStreamToken';
  17. import { getMediaToken, getStreamToken, setMediaToken, setStreamToken } from '../../api/client';
  18. const auth = {
  19. authEnabled: true,
  20. user: { id: 7 } as { id: number } | null,
  21. loading: false,
  22. granted: ['library:read_own'] as string[],
  23. };
  24. vi.mock('../../contexts/AuthContext', () => ({
  25. useAuth: () => ({
  26. authEnabled: auth.authEnabled,
  27. user: auth.user,
  28. loading: auth.loading,
  29. hasPermission: (p: string) => auth.granted.includes(p),
  30. }),
  31. }));
  32. function wrapper() {
  33. const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
  34. return ({ children }: { children: ReactNode }) => (
  35. <QueryClientProvider client={qc}>{children}</QueryClientProvider>
  36. );
  37. }
  38. let mediaMints = 0;
  39. let cameraMints = 0;
  40. beforeEach(() => {
  41. mediaMints = 0;
  42. cameraMints = 0;
  43. auth.authEnabled = true;
  44. auth.user = { id: 7 };
  45. auth.loading = false;
  46. auth.granted = ['library:read_own'];
  47. server.use(
  48. http.post('*/api/v1/auth/media-token', () => {
  49. mediaMints += 1;
  50. return HttpResponse.json({ token: 'media-tok' });
  51. }),
  52. http.post('*/api/v1/printers/camera/stream-token', () => {
  53. cameraMints += 1;
  54. return HttpResponse.json({ token: 'camera-tok' });
  55. })
  56. );
  57. });
  58. afterEach(() => {
  59. cleanup();
  60. setMediaToken(null);
  61. setStreamToken(null);
  62. });
  63. describe('useStreamTokenSync token split (#3025)', () => {
  64. it('gives a user without camera:view a media token', async () => {
  65. renderHook(() => useStreamTokenSync(), { wrapper: wrapper() });
  66. await waitFor(() => expect(getMediaToken()).toBe('media-tok'));
  67. });
  68. it('does not ask the camera mint for a user without camera:view', async () => {
  69. renderHook(() => useStreamTokenSync(), { wrapper: wrapper() });
  70. await waitFor(() => expect(mediaMints).toBe(1));
  71. // The 403 this used to produce on every page load was the reporter's only
  72. // clue that thumbnails were gated on the camera at all.
  73. expect(cameraMints).toBe(0);
  74. expect(getStreamToken()).toBeNull();
  75. });
  76. it('fetches both tokens for a user who does have camera:view', async () => {
  77. auth.granted = ['library:read_own', 'camera:view'];
  78. renderHook(() => useStreamTokenSync(), { wrapper: wrapper() });
  79. await waitFor(() => expect(getMediaToken()).toBe('media-tok'));
  80. await waitFor(() => expect(getStreamToken()).toBe('camera-tok'));
  81. });
  82. it('fetches nothing while auth is still bootstrapping', async () => {
  83. auth.loading = true;
  84. renderHook(() => useStreamTokenSync(), { wrapper: wrapper() });
  85. await new Promise((r) => setTimeout(r, 20));
  86. expect(mediaMints).toBe(0);
  87. expect(cameraMints).toBe(0);
  88. });
  89. it('fetches nothing when auth is enabled and nobody is signed in', async () => {
  90. auth.user = null;
  91. renderHook(() => useStreamTokenSync(), { wrapper: wrapper() });
  92. await new Promise((r) => setTimeout(r, 20));
  93. expect(mediaMints).toBe(0);
  94. expect(cameraMints).toBe(0);
  95. });
  96. it('fetches both when auth is disabled, where neither mint is gated', async () => {
  97. auth.authEnabled = false;
  98. auth.user = null;
  99. auth.granted = [];
  100. renderHook(() => useStreamTokenSync(), { wrapper: wrapper() });
  101. await waitFor(() => expect(getMediaToken()).toBe('media-tok'));
  102. await waitFor(() => expect(getStreamToken()).toBe('camera-tok'));
  103. });
  104. });