Files
bambuddy/frontend/src/__tests__/hooks/useCameraStreamToken.test.ts
T
maziggy 32c0b169bd fix(frontend): thumbnails blank until reload after sign-in
On auth-enabled instances, logging out and back in left the File Manager
  (and occasionally the Archives page) full of broken thumbnails until a
  manual page reload. Thumbnail URLs are gated by a short-lived camera
  stream token that <img> tags cannot send via Authorization headers, so
  the token is appended as ?token=… at render time.

  Two races broke this after sign-in:

  1. The token query was keyed on ['camera-stream-token'] alone and fired
     while the user was still on the login page. It 401'd, React Query
     cached the failure with a 50-minute staleTime, and nothing invalidated
     it after login — the token never arrived.

  2. Even when the token did arrive, the module-level variable holding it
     was not reactive, so pages that had already rendered kept serving
     image URLs with no token in them.

  Fixes:

  - Include user.id in the query key and gate with
    `enabled: authEnabled ? !!user : true`. A new sign-in produces a new
    key and triggers a fresh fetch; no anonymous fetch is cached.
  - When the token transitions from null to a value, walk the DOM once
    and update src on every <img>/<video> pointing at /api/v1/ without
    the current token so already-rendered pages reload in place.
  - Mirror the query key/gate in CameraPage so it shares the cache entry.

  The DOM-rewrite logic is extracted into rewriteMediaSrcWithToken() with
  unit tests covering: appending to a query-less URL, & separator with an
  existing query, skipping URLs that already carry the current token,
  replacing a stale token (trailing and middle positions), leaving
  non-/api/v1/ URLs alone, updating <video>, and URL-encoding tokens with
  special characters.
2026-04-19 13:27:41 +02:00

87 lines
3.3 KiB
TypeScript

/**
* Unit tests for rewriteMediaSrcWithToken — the DOM walker that retrofits a
* camera stream token onto <img>/<video> src URLs that rendered before the
* token arrived (regression guard for the post-login blank-thumbnails bug).
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { rewriteMediaSrcWithToken } from '../../hooks/useCameraStreamToken';
describe('rewriteMediaSrcWithToken', () => {
let root: HTMLDivElement;
beforeEach(() => {
root = document.createElement('div');
document.body.appendChild(root);
});
afterEach(() => {
root.remove();
});
const addImg = (src: string) => {
const img = document.createElement('img');
img.setAttribute('src', src);
root.appendChild(img);
return img;
};
const addVideo = (src: string) => {
const v = document.createElement('video');
v.setAttribute('src', src);
root.appendChild(v);
return v;
};
it('appends token to /api/v1/ images that have no query string', () => {
const img = addImg('/api/v1/library/files/42/thumbnail');
const count = rewriteMediaSrcWithToken(root, 'abc123');
expect(count).toBe(1);
expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=abc123');
});
it('appends token to URLs that already have a query string using & separator', () => {
const img = addImg('/api/v1/archives/5/thumbnail?v=1700000000000');
rewriteMediaSrcWithToken(root, 'abc123');
expect(img.getAttribute('src')).toBe('/api/v1/archives/5/thumbnail?v=1700000000000&token=abc123');
});
it('leaves images alone that already carry the current token', () => {
const img = addImg('/api/v1/library/files/42/thumbnail?token=abc123');
const count = rewriteMediaSrcWithToken(root, 'abc123');
expect(count).toBe(0);
expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=abc123');
});
it('replaces a stale token with the current one', () => {
const img = addImg('/api/v1/library/files/42/thumbnail?token=OLD');
rewriteMediaSrcWithToken(root, 'NEW');
expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=NEW');
});
it('replaces a stale token that sits in the middle of the query string', () => {
const img = addImg('/api/v1/archives/5/thumbnail?token=OLD&v=1700000000000');
rewriteMediaSrcWithToken(root, 'NEW');
// Old token stripped, v preserved, new token appended.
expect(img.getAttribute('src')).toBe('/api/v1/archives/5/thumbnail?v=1700000000000&token=NEW');
});
it('ignores images that do not point at /api/v1/', () => {
const img = addImg('https://cdn.example.com/static/logo.png');
rewriteMediaSrcWithToken(root, 'abc123');
expect(img.getAttribute('src')).toBe('https://cdn.example.com/static/logo.png');
});
it('updates <video> elements as well', () => {
const v = addVideo('/api/v1/printers/7/camera/stream?fps=10');
rewriteMediaSrcWithToken(root, 'abc123');
expect(v.getAttribute('src')).toBe('/api/v1/printers/7/camera/stream?fps=10&token=abc123');
});
it('url-encodes tokens containing special characters', () => {
const img = addImg('/api/v1/library/files/42/thumbnail');
rewriteMediaSrcWithToken(root, 'a b/c=d');
expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=a%20b%2Fc%3Dd');
});
});