Files
readest/apps/readest-app/src/__tests__/middleware.test.ts
T
Huang Xin 1ea607829c fix(share): load cover under COEP, keep share links out of the clipper, fix in-app import (#4636)
Three issues found while debugging shared-book links:

- /s cover was a broken <img>: the page runs under COEP: require-corp (for
  Turso SharedArrayBuffer), and the cover redirects to a cross-origin R2
  presigned URL that can't carry a Cross-Origin-Resource-Policy header, so the
  browser blocked it. R2 already has CORS, but that's a different header — a
  plain no-cors <img> needs CORP, which presigned URLs can't set. Serve /s with
  COEP: credentialless, which keeps the page cross-origin isolated (the Turso
  replica still boots there) while allowing the image. Scoped to /s; every other
  route keeps require-corp.

- Android share links (https://web.readest.com/s/{token}) were run through the
  article clipper: useClipUrlIngress excluded annotation links but not share
  links, so they fell through to clip_url/readability. Skip parseShareDeepLink
  URLs — useOpenShareLink owns that path.

- In-app book import failed with "Origin null is not allowed": the importer
  fetched /share/{token}/download with the renderer's fetch, and on the app
  (tauri.localhost -> web -> R2) the second cross-origin redirect nulls the
  request Origin, which R2's CORS rejects. Use the native HTTP client
  (tauriFetch) on the app — it follows the redirect and ignores CORS, needs no
  server change, and works against the deployed server. Web is unaffected: its
  fetch's redirect to R2 is the first cross-origin hop, so the Origin is
  preserved and R2 allows it.

Adds unit tests (middleware COEP per route, clipper skips share links, download
route 302, importer uses native HTTP on app and the renderer fetch on web).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 06:18:28 +02:00

36 lines
1.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { NextRequest } from 'next/server';
import { middleware } from '@/middleware';
const coep = (path: string) =>
middleware(new NextRequest(`http://localhost:3000${path}`)).headers.get(
'Cross-Origin-Embedder-Policy',
);
describe('middleware cross-origin isolation headers', () => {
it('serves COEP credentialless on the /s share landing so the R2 cover <img> loads', () => {
// The cover redirects to a cross-origin R2 URL that can't carry a CORP
// header; credentialless keeps the page isolated while allowing it.
expect(coep('/s')).toBe('credentialless');
expect(coep('/s/Qmup0X1A8ovl2FmKJKA8mB')).toBe('credentialless');
});
it('keeps the stricter require-corp on every other document route', () => {
expect(coep('/')).toBe('require-corp');
expect(coep('/library')).toBe('require-corp');
// Must not be caught by a naive startsWith('/s').
expect(coep('/settings')).toBe('require-corp');
expect(coep('/search')).toBe('require-corp');
});
it('always pairs COOP same-origin on document responses', () => {
const res = middleware(new NextRequest('http://localhost:3000/s/tok'));
expect(res.headers.get('Cross-Origin-Opener-Policy')).toBe('same-origin');
});
it('does not put COEP on /api routes', () => {
const res = middleware(new NextRequest('http://localhost:3000/api/share/tok'));
expect(res.headers.get('Cross-Origin-Embedder-Policy')).toBeNull();
});
});