import { describe, expect, test } from 'bun:test'; import { rewriteHtml, rewriteLocation, rewriteSetCookie, rewriteAttributes, rewriteAppSubUrl, rewriteAssetPaths, } from './rewrite'; const BASE = '/api/gitea/_web'; const ORIGINS = ['http://localhost:3000', 'https://gitea.officer.dev']; // Every test here is a way the frame breaks in a manner that looks like Gitea being broken rather than // the proxy. That is the whole difficulty: the page renders, and then a click, a login or an asset goes // somewhere Officer knows nothing about. describe('attributes', () => { test('root-absolute href, src and action are prefixed', () => { const html = '
'; const out = rewriteAttributes(html, BASE); expect(out).toContain(`href="${BASE}/user/login"`); expect(out).toContain(`src="${BASE}/assets/img/logo.svg"`); expect(out).toContain(`action="${BASE}/repo/new"`); }); // Gitea hands URLs to its own JavaScript through data attributes, which is where the dashboard heatmap // gets its endpoint — `document.querySelector('#user-heatmap').getAttribute('data-heatmap-url')`. // Unprefixed it fetched from the PLATFORM's origin, got the SPA's index.html, and reported // "Unexpected token '<'" while parsing JSON. Found by reading Gitea's bundle, not by guessing. test('data-*url attributes are rewritten — the heatmap reads its endpoint from one', () => { const html = '
'; expect(rewriteAttributes(html, BASE)).toContain(`data-heatmap-url="${BASE}/api/v1/users/x/heatmap"`); }); test('and plain data-url, which the language switcher uses', () => { expect(rewriteAttributes('', BASE)).toContain(`data-url="${BASE}/?lang=en-US"`); }); test('a data attribute that is not a url is left alone', () => { const html = '
'; expect(rewriteAttributes(html, BASE)).toBe(html); }); test('a relative href is already correct and must not be touched', () => { expect(rewriteAttributes('', BASE)).toBe(''); }); test('mailto, data and protocol-relative are left alone', () => { const html = ''; expect(rewriteAppSubUrl(html, BASE)).toContain(`"appSubUrl":"${BASE}"`); }); test('an existing subpath is replaced, not appended to', () => { expect(rewriteAppSubUrl('{"appSubUrl":"/git"}', BASE)).toBe(`{"appSubUrl":"${BASE}"}`); }); // The shape Gitea ACTUALLY emits — a JavaScript object literal, unquoted key, single quotes. The first // version of this only matched the JSON form, so it silently did nothing on every real page while the // attributes around it rewrote perfectly. Taken verbatim from a live response. test('the JS object-literal form Gitea really emits', () => { const real = "window.config = {\n appUrl: 'https://gitea.officer.dev/',\n appSubUrl: '',\n};"; const out = rewriteAppSubUrl(real, BASE); expect(out).toContain(`appSubUrl: '${BASE}'`); // The quote style is preserved, or the literal stops parsing. expect(out).not.toContain(`appSubUrl: "${BASE}"`); }); }); describe('absolute URLs built from ROOT_URL', () => { test('the upstream origin becomes the proxy base — these escape to a third-party origin otherwise', () => { const out = rewriteHtml('', BASE, ORIGINS); expect(out).toContain(`href="${BASE}/owner/repo"`); expect(out).not.toContain('gitea.officer.dev'); }); test('the loopback origin is rewritten too, since that is what the sidecar actually fetches', () => { expect(rewriteHtml('', BASE, ORIGINS)).toContain(`${BASE}/explore`); }); test('the JSON-ESCAPED form, which is how appUrl appears in Gitea\'s inline script', () => { // `appUrl: 'https:\\/\\/gitea.officer.dev\\/'`. Missed at first, and the failure was quiet: the page // rendered fine while every URL Gitea's JS built from appUrl went cross-origin to the real host and // came back as its login HTML — so the heatmap reported "Unexpected token '<'" parsing JSON. const real = String.raw`appUrl: 'https:\/\/gitea.officer.dev\/',`; const out = rewriteHtml(real, BASE, ORIGINS); expect(out).toContain(String.raw`'\/api\/gitea\/_web\/'`); expect(out).not.toContain('gitea.officer.dev'); }); test('an unrelated host is left alone', () => { const html = ''; expect(rewriteHtml(html, BASE, ORIGINS)).toBe(html); }); }); describe('redirects — the most visible failure, because the frame just leaves', () => { test('a root-absolute Location is prefixed', () => { expect(rewriteLocation('/user/login', BASE, ORIGINS)).toBe(`${BASE}/user/login`); }); test('a full URL on the upstream origin is brought back inside', () => { expect(rewriteLocation('https://gitea.officer.dev/dashboard', BASE, ORIGINS)).toBe(`${BASE}/dashboard`); }); test('an already-prefixed Location is not prefixed twice', () => { expect(rewriteLocation(`${BASE}/dashboard`, BASE, ORIGINS)).toBe(`${BASE}/dashboard`); }); test('an external redirect is left alone — OAuth goes where it goes', () => { expect(rewriteLocation('https://accounts.google.com/o/oauth2/auth', BASE, ORIGINS)).toBe( 'https://accounts.google.com/o/oauth2/auth', ); }); }); describe('asset paths baked into the page before Gitea\'s own code runs', () => { test('the JSON-escaped form — the SharedWorker URL that broke the dashboard', () => { // Verbatim from a live page. Not an attribute, and its slashes are backslash-escaped, so neither the // attribute rewrite nor appSubUrl touches it. Unprefixed, the browser resolves it against the // PLATFORM's origin, gets the SPA's index.html, and reports `Unexpected token '<'` from a file that // looks like it loaded. const real = String.raw`'\/assets\/js\/eventsource.sharedworker.DV1Z2uJv.js',`; const out = rewriteAssetPaths(real, BASE); expect(out).toContain(String.raw`'\/api\/gitea\/_web\/assets\/js\/eventsource`); // The escaping has to match the string it lands in, or the JS stops parsing. expect(out).not.toContain(String.raw`\\/api`); }); test('the plain quoted form too', () => { expect(rewriteAssetPaths(`x = "/assets/img/logo.svg"`, BASE)).toContain(`"${BASE}/assets/img/logo.svg"`); }); test('leaves a path that is not an asset root alone', () => { expect(rewriteAssetPaths(`x = "/user/login"`, BASE)).toBe(`x = "/user/login"`); }); }); describe('the origin Gitea declares about itself', () => { test('is discovered from window.config, so ROOT_URL needs no second setting', () => { // The sidecar only knows where it FETCHES from — usually loopback. The public origin lives in the // page, and an unrewritten absolute link on it navigates the frame off to a third-party origin where // the session cookie is dropped. const html = '' + 'go'; const out = rewriteHtml(html, BASE, ['http://localhost:3000']); expect(out).toContain(`href="${BASE}/owner/repo"`); expect(out).not.toContain('git.example.com/owner'); }); test('a page without one still rewrites what it was told about', () => { expect(rewriteHtml('', BASE, ['http://localhost:3000'])).toContain( `href="${BASE}/x"`, ); }); }); describe('cookies', () => { test('Path is narrowed to the frame, so Gitea session cookies stay out of the platform API', () => { expect(rewriteSetCookie('i_like_gitea=abc; Path=/; HttpOnly', BASE)).toBe( `i_like_gitea=abc; Path=${BASE}; HttpOnly`, ); }); test('a cookie with no Path gets one', () => { expect(rewriteSetCookie('csrf=xyz; HttpOnly', BASE)).toBe(`csrf=xyz; HttpOnly; Path=${BASE}`); }); test('Secure is left exactly as Gitea set it — adding it breaks http, removing it weakens https', () => { expect(rewriteSetCookie('s=1; Path=/; Secure; SameSite=Lax', BASE)).toContain('Secure'); expect(rewriteSetCookie('s=1; Path=/', BASE)).not.toContain('Secure'); }); });