useCameraStreamToken.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * Unit tests for rewriteMediaSrcWithToken — the DOM walker that retrofits a
  3. * query token onto <img>/<video> src URLs that rendered before the token
  4. * arrived (regression guard for the post-login blank-thumbnails bug).
  5. *
  6. * Since #3025 it carries two tokens and picks per URL: live-camera URLs take
  7. * the camera stream token, everything else takes the media token.
  8. */
  9. import { afterEach, beforeEach, describe, expect, it } from 'vitest';
  10. import { isCameraUrl, rewriteMediaSrcWithToken } from '../../hooks/useCameraStreamToken';
  11. describe('rewriteMediaSrcWithToken', () => {
  12. let root: HTMLDivElement;
  13. beforeEach(() => {
  14. root = document.createElement('div');
  15. document.body.appendChild(root);
  16. });
  17. afterEach(() => {
  18. root.remove();
  19. });
  20. const addImg = (src: string) => {
  21. const img = document.createElement('img');
  22. img.setAttribute('src', src);
  23. root.appendChild(img);
  24. return img;
  25. };
  26. const addVideo = (src: string) => {
  27. const v = document.createElement('video');
  28. v.setAttribute('src', src);
  29. root.appendChild(v);
  30. return v;
  31. };
  32. it('appends token to /api/v1/ images that have no query string', () => {
  33. const img = addImg('/api/v1/library/files/42/thumbnail');
  34. const count = rewriteMediaSrcWithToken(root, 'abc123', null);
  35. expect(count).toBe(1);
  36. expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=abc123');
  37. });
  38. it('appends token to URLs that already have a query string using & separator', () => {
  39. const img = addImg('/api/v1/archives/5/thumbnail?v=1700000000000');
  40. rewriteMediaSrcWithToken(root, 'abc123', null);
  41. expect(img.getAttribute('src')).toBe('/api/v1/archives/5/thumbnail?v=1700000000000&token=abc123');
  42. });
  43. it('leaves images alone that already carry the current token', () => {
  44. const img = addImg('/api/v1/library/files/42/thumbnail?token=abc123');
  45. const count = rewriteMediaSrcWithToken(root, 'abc123', null);
  46. expect(count).toBe(0);
  47. expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=abc123');
  48. });
  49. it('replaces a stale token with the current one', () => {
  50. const img = addImg('/api/v1/library/files/42/thumbnail?token=OLD');
  51. rewriteMediaSrcWithToken(root, 'NEW', null);
  52. expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=NEW');
  53. });
  54. it('replaces a stale token that sits in the middle of the query string', () => {
  55. const img = addImg('/api/v1/archives/5/thumbnail?token=OLD&v=1700000000000');
  56. rewriteMediaSrcWithToken(root, 'NEW', null);
  57. // Old token stripped, v preserved, new token appended.
  58. expect(img.getAttribute('src')).toBe('/api/v1/archives/5/thumbnail?v=1700000000000&token=NEW');
  59. });
  60. it('ignores images that do not point at /api/v1/', () => {
  61. const img = addImg('https://cdn.example.com/static/logo.png');
  62. rewriteMediaSrcWithToken(root, 'abc123', null);
  63. expect(img.getAttribute('src')).toBe('https://cdn.example.com/static/logo.png');
  64. });
  65. it('updates <video> elements as well', () => {
  66. const v = addVideo('/api/v1/printers/7/camera/stream?fps=10');
  67. rewriteMediaSrcWithToken(root, null, 'abc123');
  68. expect(v.getAttribute('src')).toBe('/api/v1/printers/7/camera/stream?fps=10&token=abc123');
  69. });
  70. it('url-encodes tokens containing special characters', () => {
  71. const img = addImg('/api/v1/library/files/42/thumbnail');
  72. rewriteMediaSrcWithToken(root, 'a b/c=d', null);
  73. expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=a%20b%2Fc%3Dd');
  74. });
  75. });
  76. // #3025 — the two tokens are not interchangeable. A user without camera:view
  77. // holds a media token and no camera token; sending the media token to a camera
  78. // route (or the camera token to a thumbnail) would 401 either way.
  79. describe('rewriteMediaSrcWithToken picks the token per URL (#3025)', () => {
  80. let root: HTMLDivElement;
  81. beforeEach(() => {
  82. root = document.createElement('div');
  83. document.body.appendChild(root);
  84. });
  85. afterEach(() => {
  86. root.remove();
  87. });
  88. const addImg = (src: string) => {
  89. const img = document.createElement('img');
  90. img.setAttribute('src', src);
  91. root.appendChild(img);
  92. return img;
  93. };
  94. it('gives a thumbnail the media token, not the camera token', () => {
  95. const img = addImg('/api/v1/library/files/42/thumbnail');
  96. rewriteMediaSrcWithToken(root, 'media-tok', 'camera-tok');
  97. expect(img.getAttribute('src')).toBe('/api/v1/library/files/42/thumbnail?token=media-tok');
  98. });
  99. it('gives a live camera stream the camera token, not the media token', () => {
  100. const img = addImg('/api/v1/printers/7/camera/stream?fps=10');
  101. rewriteMediaSrcWithToken(root, 'media-tok', 'camera-tok');
  102. expect(img.getAttribute('src')).toBe('/api/v1/printers/7/camera/stream?fps=10&token=camera-tok');
  103. });
  104. it('still rewrites thumbnails for a user who has no camera token at all', () => {
  105. const thumb = addImg('/api/v1/archives/5/thumbnail');
  106. const stream = addImg('/api/v1/printers/7/camera/stream?fps=10');
  107. const count = rewriteMediaSrcWithToken(root, 'media-tok', null);
  108. expect(count).toBe(1);
  109. expect(thumb.getAttribute('src')).toBe('/api/v1/archives/5/thumbnail?token=media-tok');
  110. // Left untouched rather than given a token that would not work on it.
  111. expect(stream.getAttribute('src')).toBe('/api/v1/printers/7/camera/stream?fps=10');
  112. });
  113. it('classifies the three camera routes as camera and the media routes as media', () => {
  114. expect(isCameraUrl('/api/v1/printers/1/camera/stream?fps=10')).toBe(true);
  115. expect(isCameraUrl('/api/v1/printers/1/camera/snapshot')).toBe(true);
  116. expect(isCameraUrl('/api/v1/printers/1/camera/plate-detection/references/0/thumbnail')).toBe(true);
  117. expect(isCameraUrl('/api/v1/library/files/42/thumbnail')).toBe(false);
  118. expect(isCameraUrl('/api/v1/archives/5/timelapse')).toBe(false);
  119. expect(isCameraUrl('/api/v1/printers/1/cover')).toBe(false);
  120. expect(isCameraUrl('/api/v1/external-links/3/icon')).toBe(false);
  121. });
  122. });