Heimdall/tests/browser-adapter.test.ts

109 lines
4.2 KiB
TypeScript
Raw Permalink Normal View History

import { afterEach, describe, expect, it, vi } from "vitest";
import {
heimdallAccessBrowserAdapter,
readHeimdallBrowserReturn,
resumeHeimdallAccess,
} from "../plugins/gamecult.heimdall.access/browser-adapter.js";
const attemptKey = "gamecult.heimdall.access.attempt";
afterEach(() => {
vi.unstubAllGlobals();
});
describe("Heimdall browser completion fallback", () => {
it("recognizes only a matching Ghostlight browser completion witness", () => {
const handle = "d0a9c1e7-89ac-46f9-8c85-c2484ed18f76";
expect(readHeimdallBrowserReturn(returnUrl(handle), "ghostlight")).toEqual({
status: "success",
handle,
});
expect(readHeimdallBrowserReturn(returnUrl(handle), "another-app")).toMatchObject({ status: "error" });
// The completion code is a distinct secret; the handle is the attempt id
// and the code never leaves the fragment.
expect(readHeimdallBrowserReturn(returnUrl(handle, "secret-redeem-code"), "ghostlight")).toEqual({
status: "success",
handle,
});
expect(JSON.stringify(readHeimdallBrowserReturn(returnUrl(handle, "secret-redeem-code"), "ghostlight")))
.not.toContain("secret-redeem-code");
expect(readHeimdallBrowserReturn(returnUrl("", "secret-redeem-code"), "ghostlight")).toMatchObject({ status: "error" });
expect(readHeimdallBrowserReturn("https://yggdrasil.gamecult.org/ghostlight/", "ghostlight")).toBeUndefined();
});
it("redeems the URL witness after tab-local attempt state is lost", async () => {
const handle = "0be30b8c-b599-4920-b8b8-733f5e7bc275";
const storage = memoryStorage();
const replaced: string[] = [];
vi.stubGlobal("sessionStorage", storage);
vi.stubGlobal("history", { replaceState: (_state: unknown, _title: string, url: string) => replaced.push(url) });
vi.stubGlobal("window", {
location: {
href: returnUrl(handle),
pathname: "/ghostlight/",
search: "",
},
});
const completed: string[] = [];
await expect(resumeHeimdallAccess({ complete: async value => { completed.push(value); } }, { appSlug: "ghostlight" }))
.resolves.toBe(true);
expect(completed).toEqual([handle]);
expect(storage.getItem(attemptKey)).toBe(handle);
expect(replaced).toEqual(["/ghostlight/"]);
await heimdallAccessBrowserAdapter.consumeCommandResult?.({
schemaId: "heimdall.auth_completion_status.v1",
payload: { status: "authenticated" },
});
expect(storage.getItem(attemptKey)).toBeNull();
});
it("does not redeem a returned witness that conflicts with the browser-held attempt", async () => {
const storage = memoryStorage([[attemptKey, "stored-handle"]]);
vi.stubGlobal("sessionStorage", storage);
vi.stubGlobal("history", { replaceState: () => undefined });
vi.stubGlobal("window", {
location: {
href: returnUrl("returned-handle"),
pathname: "/ghostlight/",
search: "",
},
});
const complete = vi.fn(async () => undefined);
await expect(resumeHeimdallAccess({ complete }, { appSlug: "ghostlight" }))
.rejects.toThrow("other than the one this browser started");
expect(complete).not.toHaveBeenCalled();
expect(storage.getItem(attemptKey)).toBeNull();
});
});
// Heimdall issues a random completion code distinct from the attempt id
// (src/app.ts createAuthCompletion); the fixture must too, or it restates the
// contract this adapter used to assume.
function returnUrl(attemptId: string, completionCode = `code-for-${attemptId}`): string {
const parameters = new URLSearchParams({
heimdall_status: "success",
heimdall_provider: "discord",
heimdall_handoff_kind: "browser_completion",
heimdall_app_slug: "ghostlight",
heimdall_mode: "sign_in",
heimdall_attempt_id: attemptId,
heimdall_completion_code: completionCode,
});
return `https://yggdrasil.gamecult.org/ghostlight/#${parameters}`;
}
function memoryStorage(initial: Array<[string, string]> = []): Storage {
const values = new Map(initial);
return {
get length() { return values.size; },
clear: () => values.clear(),
getItem: key => values.get(key) ?? null,
key: index => [...values.keys()][index] ?? null,
removeItem: key => { values.delete(key); },
setItem: (key, value) => { values.set(key, value); },
};
}