import { describe, expect, test } from 'bun:test'; import { rewriteHtml, rewriteLocation, rewriteSetCookie, rewriteAttributes, rewriteAppSubUrl } 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"`); }); 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}"}`); }); }); 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('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('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'); }); });