Something went wrong. Try again.
Find the cast and crew shared by two movies or TV shows crosscredit.haltcase.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { previewName } from "../../scripts/deployment.ts";import { commentOnPreview } from "../../scripts/preview-comment.ts";
const branch = "feature/search";const sha = "a".repeat(40);const repo = "did:plc:repository";const pull = `at://${repo}/org.tangled.track.ticket/3example`;const previewUrl = "https://pr-search-crosscredit.example.workers.dev";
const output = JSON.stringify({ type: "preview", version: 1, preview_name: previewName(branch), preview_urls: [previewUrl], deployment_urls: []});
const fetchMock = vi.fn<typeof fetch>();const logMock = vi.fn();
function requestUrl(input: RequestInfo | URL | undefined): URL { if (input instanceof URL) { return input; }
if (input instanceof Request) { return new URL(input.url); }
if (typeof input === "string") { return new URL(input); }
throw new Error("Missing request URL");}
const pipeline = { id: "pipeline-id", repo, commit: sha, trigger: { $type: "org.tangled.event.pullRequest", pull, sourceBranch: branch, sourceSha: sha, targetBranch: "main" }};
const ticket = { uri: pull, cid: "current-ticket-cid", value: { $type: "org.tangled.track.ticket", state: "open", targetBranch: "main" }};
beforeEach(() => { vi.clearAllMocks(); vi.stubGlobal("fetch", fetchMock); vi.stubEnv("TANGLED_COMMENT_APP_PASSWORD", "test-app-password"); vi.stubEnv("TANGLED_COMMENT_PDS_URL", "https://pds.example.com"); vi.stubEnv("TANGLED_COMMENT_IDENTIFIER", "preview-bot.example.com"); vi.stubEnv("TANGLED_COMMENT_SPINDLE_URL", "https://spindle.example.com"); vi.stubEnv("TANGLED_REPO_KNOT", "knot.example.com"); vi.stubEnv("TANGLED_REPO_REPO_DID", repo); vi.stubEnv("TANGLED_PIPELINE_ID", "pipeline-id"); vi.spyOn(console, "info").mockImplementation(logMock);
fetchMock.mockImplementation(async (input) => { switch (requestUrl(input).pathname) { case "/xrpc/org.tangled.ci.getPipeline": { return Response.json(pipeline); } case "/xrpc/com.atproto.repo.getRecord": { return Response.json(ticket); } case "/xrpc/com.atproto.server.createSession": { return Response.json({ accessJwt: "test-access-token" }); } case "/xrpc/com.atproto.server.getServiceAuth": { return Response.json({ token: "test-service-token" }); } case "/xrpc/com.atproto.repo.createRecord": { return Response.json({ uri: "at://comment", cid: "comment-cid" }); } default: { throw new Error("Unexpected API call"); } } });});
afterEach(() => { vi.useRealTimers(); vi.unstubAllEnvs(); vi.unstubAllGlobals(); vi.restoreAllMocks();});
describe("PR preview comments", () => { it("posts the returned Preview URL to the exact triggering PR using a method-scoped service token", async () => { await commentOnPreview(output, branch, sha); expect(fetchMock).toHaveBeenCalledTimes(5);
const [pipelineUrl] = fetchMock.mock.calls[0] ?? [];
expect(requestUrl(pipelineUrl).href).toBe( `https://spindle.example.com/xrpc/org.tangled.ci.getPipeline?repo=${encodeURIComponent(repo)}&pipeline=pipeline-id` );
const [authUrl, authOptions] = fetchMock.mock.calls[3] ?? [];
expect(requestUrl(authUrl).searchParams.get("aud")).toBe(repo); expect(requestUrl(authUrl).searchParams.get("lxm")).toBe("com.atproto.repo.createRecord"); expect(authOptions?.headers).toEqual({ Authorization: "Bearer test-access-token" });
const [commentUrl, options] = fetchMock.mock.calls[4] ?? [];
expect(requestUrl(commentUrl).href).toBe( "https://knot.example.com/xrpc/com.atproto.repo.createRecord" );
expect(options?.headers).toEqual({ "Content-Type": "application/json", Authorization: "Bearer test-service-token" });
expect(options?.redirect).toBe("error");
const body = options?.body;
if (typeof body !== "string") { throw new TypeError("Expected a JSON comment body"); }
expect(JSON.parse(body)).toMatchObject({ repo, collection: "org.tangled.feed.comment", record: { $type: "org.tangled.feed.comment", subject: { $type: "com.atproto.repo.strongRef", uri: pull, cid: "current-ticket-cid" }, body: { $type: "org.tangled.markup.markdown", text: `Preview ready: [Open preview](${previewUrl})\n\nCommit: \`${sha}\`` } } });
expect(body).not.toContain("test-app-password"); });
it("still logs the URL without any network calls when comments are not configured", async () => { vi.stubEnv("TANGLED_COMMENT_APP_PASSWORD", ""); await commentOnPreview(output, branch, sha); expect(fetchMock).not.toHaveBeenCalled(); expect(logMock).toHaveBeenCalledWith(`Preview ready: ${previewUrl}`); });
it("does not authenticate or comment after the PR has closed", async () => { fetchMock.mockResolvedValueOnce(Response.json(pipeline)).mockResolvedValueOnce( Response.json({ ...ticket, value: { ...ticket.value, state: "completed" } }) );
await commentOnPreview(output, branch, sha); expect(fetchMock).toHaveBeenCalledTimes(2); });
it.each([ { ...pipeline, repo: "did:plc:other" }, { ...pipeline, commit: "b".repeat(40) }, { ...pipeline, trigger: { ...pipeline.trigger, sourceBranch: "another-branch" } }, { ...pipeline, trigger: { ...pipeline.trigger, pull: "at://did:plc:other/org.tangled.track.ticket/3example" } } ])("refuses a mismatched pipeline or PR", async (value) => { fetchMock.mockResolvedValueOnce(Response.json(value)); await expect(commentOnPreview(output, branch, sha)).rejects.toThrow(); expect(fetchMock).toHaveBeenCalledOnce(); });
it("rejects a different PR returned by the knot before authenticating", async () => { fetchMock .mockResolvedValueOnce(Response.json(pipeline)) .mockResolvedValueOnce(Response.json({ ...ticket, uri: `${pull}other` }));
await expect(commentOnPreview(output, branch, sha)).rejects.toThrow("different PR"); expect(fetchMock).toHaveBeenCalledTimes(2); });
it.each([ "not json", JSON.stringify({ type: "preview", version: 1, preview_name: "wrong", preview_urls: [previewUrl], deployment_urls: [] }), JSON.stringify({ type: "preview", version: 1, preview_name: previewName(branch), preview_urls: ["https://unrelated.example.com"], deployment_urls: [] }) ])("refuses invalid Cloudflare output", async (value) => { await expect(commentOnPreview(value, branch, sha)).rejects.toThrow(); expect(fetchMock).not.toHaveBeenCalled(); });
it("reports authentication failure without logging the response body or credentials", async () => { fetchMock .mockResolvedValueOnce(Response.json(pipeline)) .mockResolvedValueOnce(Response.json(ticket)) .mockResolvedValueOnce(Response.json({ message: "private-response-body" }, { status: 401 }));
await expect(commentOnPreview(output, branch, sha)).rejects.toThrow( "createSession failed (HTTP 401)" );
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(JSON.stringify(logMock.mock.calls)).not.toMatch( /test-app-password|private-response-body|test-access-token/ ); });
it("recovers from a read timeout and completes the comment", async () => { vi.useFakeTimers(); fetchMock.mockRejectedValueOnce(new DOMException("private-error-detail", "TimeoutError"));
const result = commentOnPreview(output, branch, sha);
await vi.runAllTimersAsync(); await result; expect(fetchMock).toHaveBeenCalledTimes(6); expect(logMock).toHaveBeenCalledWith("Posted PR preview comment."); expect(JSON.stringify(logMock.mock.calls)).not.toContain("private-error-detail"); });
it.each([408, 429, 500, 502, 503, 504])( "retries a transient HTTP %s read response", async (status) => { vi.useFakeTimers(); fetchMock.mockResolvedValueOnce(Response.json({ message: "private-body" }, { status }));
const result = commentOnPreview(output, branch, sha);
await vi.runAllTimersAsync(); await result; expect(fetchMock).toHaveBeenCalledTimes(6); expect(JSON.stringify(logMock.mock.calls)).not.toContain("private-body"); } );
it("retries a network failure on a read", async () => { vi.useFakeTimers(); fetchMock.mockRejectedValueOnce(new TypeError("private-network-detail"));
const result = commentOnPreview(output, branch, sha);
await vi.runAllTimersAsync(); await result; expect(fetchMock).toHaveBeenCalledTimes(6); expect(JSON.stringify(logMock.mock.calls)).not.toContain("private-network-detail"); });
it.each([ [0, "https://spindle.example.com/xrpc/org.tangled.ci.getPipeline", 3], [1, "https://knot.example.com/xrpc/com.atproto.repo.getRecord", 3], [2, "https://pds.example.com/xrpc/com.atproto.server.createSession", 1], [3, "https://pds.example.com/xrpc/com.atproto.server.getServiceAuth", 3], [4, "https://knot.example.com/xrpc/com.atproto.repo.createRecord", 1] ] as const)( "identifies a timeout at step %s and bounds retries", async (step, service, attempts) => { vi.useFakeTimers();
const original = fetchMock.getMockImplementation(); let calls = 0;
fetchMock.mockImplementation((input, options) => { calls += 1;
if (calls > step) { return Promise.reject(new DOMException("private-detail", "TimeoutError")); }
if (!original) { throw new Error("Missing fetch implementation"); }
return original(input, options); });
const result = expect(commentOnPreview(output, branch, sha)).rejects.toThrow( `Tangled ${service} timed out after 30 seconds after ${attempts} attempt(s).` );
await vi.runAllTimersAsync(); await result; expect(fetchMock).toHaveBeenCalledTimes(step + attempts); } );
it("does not retry a non-transient read failure", async () => { fetchMock.mockResolvedValueOnce(Response.json({ message: "private-body" }, { status: 403 })); await expect(commentOnPreview(output, branch, sha)).rejects.toThrow("failed (HTTP 403)"); expect(fetchMock).toHaveBeenCalledOnce(); });
it("does not retry a transient comment write failure", async () => { const original = fetchMock.getMockImplementation();
fetchMock.mockImplementation((input, options) => { if (requestUrl(input).pathname.endsWith("repo.createRecord")) { return Promise.resolve(Response.json({}, { status: 503 })); }
if (!original) { throw new Error("Missing fetch implementation"); }
return original(input, options); });
await expect(commentOnPreview(output, branch, sha)).rejects.toThrow("failed (HTTP 503)"); expect(fetchMock).toHaveBeenCalledTimes(5); });
it("rejects an insecure configured service before sending credentials", async () => { vi.stubEnv("TANGLED_COMMENT_PDS_URL", "http://pds.example.com"); await expect(commentOnPreview(output, branch, sha)).rejects.toThrow("HTTPS origins"); expect(fetchMock).not.toHaveBeenCalled(); });});