mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-04 13:11:35 +02:00
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.
87 lines
3.3 KiB
TypeScript
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');
|
|
});
|
|
});
|