mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 17:50:12 +02:00
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.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user