// @vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { checkAccess, readAccessConfig } from "./policy.server"; const config = { mode: "tailscale" as const, origin: "https://deck.invalid", allowedLogin: "owner@deck.invalid", }; beforeEach(() => vi.stubEnv("TWITTER_LITE_AUTH_MODE", undefined)); afterEach(() => vi.unstubAllEnvs()); describe("access boundary", () => { it.each([undefined, "tailscale"])("requires an owner in identity mode: %s", (mode) => { vi.stubEnv("TWITTER_LITE_AUTH_MODE", mode); vi.stubEnv("TWITTER_LITE_ORIGIN", config.origin); vi.stubEnv("TWITTER_LITE_ALLOWED_LOGIN", ""); expect(readAccessConfig()).toBeNull(); }); it.each(["", "off", "NONE"])("fails closed for an unknown auth mode: %s", (mode) => { vi.stubEnv("TWITTER_LITE_AUTH_MODE", mode); vi.stubEnv("TWITTER_LITE_ORIGIN", config.origin); vi.stubEnv("TWITTER_LITE_ALLOWED_LOGIN", config.allowedLogin); expect(checkAccess(new Request(config.origin), readAccessConfig())?.status).toBe(503); }); it("allows private-proxy navigation without an identity only when explicitly configured", () => { vi.stubEnv("TWITTER_LITE_AUTH_MODE", "none"); vi.stubEnv("TWITTER_LITE_ORIGIN", config.origin); vi.stubEnv("TWITTER_LITE_ALLOWED_LOGIN", ""); const access = readAccessConfig(); expect(access).toEqual({ origin: config.origin, mode: "none" }); expect(checkAccess(new Request(config.origin), access)).toBeNull(); }); it("still requires a configured origin without identity authentication", () => { vi.stubEnv("TWITTER_LITE_AUTH_MODE", "none"); vi.stubEnv("TWITTER_LITE_ORIGIN", ""); expect(readAccessConfig()).toBeNull(); }); it.each(["POST", "PUT", "PATCH", "DELETE"])( "requires exact Origin without identity authentication for %s", (method) => { const access = { origin: config.origin, mode: "none" as const }; expect(checkAccess(new Request(config.origin, { method }), access)?.status).toBe(403); expect( checkAccess( new Request(config.origin, { method, headers: { Origin: "https://other.invalid", "Sec-Fetch-Site": "same-origin", }, }), access, )?.status, ).toBe(403); expect( checkAccess( new Request(config.origin, { method, headers: { Origin: config.origin }, }), access, ), ).toBeNull(); }, ); it("fails closed when the deployment is not configured", () => { vi.stubEnv("TWITTER_LITE_ORIGIN", ""); vi.stubEnv("TWITTER_LITE_ALLOWED_LOGIN", ""); expect(readAccessConfig()).toBeNull(); expect(checkAccess(new Request(config.origin), null)?.status).toBe(503); }); it.each(["", "https://deck.invalid/path", "http://deck.invalid", "not a URL"])( "rejects an invalid configured origin: %s", (origin) => { vi.stubEnv("TWITTER_LITE_ORIGIN", origin); vi.stubEnv("TWITTER_LITE_ALLOWED_LOGIN", config.allowedLogin); expect(readAccessConfig()).toBeNull(); }, ); it.each(["https://deck.invalid", "http://127.0.0.1:4173"])( "accepts an explicit deployment origin: %s", (origin) => { vi.stubEnv("TWITTER_LITE_ORIGIN", origin); vi.stubEnv("TWITTER_LITE_ALLOWED_LOGIN", config.allowedLogin); expect(readAccessConfig()).toEqual({ ...config, origin }); }, ); it.each([undefined, "other@deck.invalid", "owner@deck.invalid, other@deck.invalid"])( "rejects absent, foreign, or ambiguous identities: %s", (login) => { const headers = new Headers(); if (login) headers.set("Tailscale-User-Login", login); expect(checkAccess(new Request(config.origin, { headers }), config)?.status).toBe(403); }, ); it("permits owner navigation back from an OAuth provider without Origin", () => { const request = new Request(`${config.origin}/oauth/mastodon/callback?code=code`, { headers: { "Tailscale-User-Login": config.allowedLogin, "Sec-Fetch-Site": "cross-site", }, }); expect(checkAccess(request, config)).toBeNull(); }); it.each(["POST", "PUT", "PATCH", "DELETE"])( "requires exact Origin for %s even with same-origin Fetch Metadata", (method) => { const headers = { "Tailscale-User-Login": config.allowedLogin, "Sec-Fetch-Site": "same-origin", }; expect(checkAccess(new Request(config.origin, { method, headers }), config)?.status).toBe( 403, ); expect( checkAccess( new Request(config.origin, { method, headers: { ...headers, Origin: "https://other.invalid" }, }), config, )?.status, ).toBe(403); expect( checkAccess( new Request(config.origin, { method, headers: { ...headers, Origin: config.origin }, }), config, ), ).toBeNull(); }, ); });