import { isHttpError } from "@sveltejs/kit";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ClientResponseError } from "#lib/api/client.ts";
import { redirectRepoDidToKnot, redirectToKnot } from "#lib/server/gitHttp.ts";
const REPO_DID = "did:plc:j5hmlfdrwkvtxm7cjmu7j2is";
const OWNER_DID = "did:plc:wshs7t2adsemcrrd4snkeqli";
const mocks = vi.hoisted(() => ({
resolveRepoByName: vi.fn(),
getRepoByRepoDid: vi.fn()
}));
vi.mock("#lib/server/config.ts", () => ({
getConfig: () => ({
bobbinUrl: "https://bobbin.test",
posthogApiKey: "phc_test",
posthogHost: "https://posthog.test",
posthogEnv: "test"
})
}));
vi.mock("#lib/api/identity.ts", () => ({
resolveMiniDoc: async () => ({ did: OWNER_DID, handle: "tangled.org" })
}));
vi.mock("#lib/api/repo.ts", () => ({ resolveRepoByName: mocks.resolveRepoByName }));
vi.mock("#lib/api/records.ts", () => ({ getRepoByRepoDid: mocks.getRepoByRepoDid }));
const repoView = (knot?: string) => ({
uri: `at://${REPO_DID}/org.tangled.repo.manifest/self`,
knot,
value: { declaration: { owner: OWNER_DID, name: "core" } }
});
const event =
(params: P, path: string) => ({
params,
url: new URL(`https://tangled.test${path}`),
fetch: vi.fn()
});
const statusOf = (promise: Promise) =>
promise.then(
() => "resolved",
(cause) => (isHttpError(cause) ? cause.status : cause)
);
const captured = vi.fn();
beforeEach(() => {
mocks.resolveRepoByName.mockReset().mockResolvedValue(repoView("knot1.tangled.sh"));
mocks.getRepoByRepoDid.mockReset().mockResolvedValue(repoView("knot1.tangled.sh"));
captured.mockReset().mockResolvedValue(new Response("{}"));
vi.stubGlobal("fetch", captured);
});
const sentEvents = () =>
captured.mock.calls.map(
([, init]) => JSON.parse(String(init?.body)) as Record
);
describe("clone tracking", () => {
it.each([
[
"by name",
() =>
redirectToKnot("info/refs")(
event(
{ handle: "tangled.org", repo: "core" },
"/tangled.org/core/info/refs?service=git-upload-pack"
)
)
],
[
"by repo did",
() =>
redirectRepoDidToKnot("info/refs")(
event({ handle: REPO_DID }, `/${REPO_DID}/info/refs?service=git-upload-pack`)
)
]
])("counts an upload-pack ref advertisement %s against the owner", async (_, run) => {
await run();
expect(sentEvents()).toEqual([
expect.objectContaining({
event: "clone",
distinct_id: OWNER_DID,
properties: {
repo: "core",
repo_at: `at://${REPO_DID}/org.tangled.repo.manifest/self`,
environment: "test"
}
})
]);
});
it.each([
["a push", "info/refs", "?service=git-receive-pack"],
["the pack itself", "git-upload-pack", ""],
["an archive", "info/refs", "?service=git-upload-archive"]
])("leaves %s uncounted", async (_, service, search) => {
await redirectToKnot(service)(
event({ handle: "tangled.org", repo: "core" }, `/tangled.org/core/${service}${search}`)
);
expect(captured).not.toHaveBeenCalled();
});
});
describe("redirectToKnot", () => {
it.each([
["info/refs", "?service=git-upload-pack"],
["info/refs", "?service=git-receive-pack"],
["git-upload-pack", ""],
["git-receive-pack", ""]
])("sends %s%s to the repo did on its knot", async (service, search) => {
const res = await redirectToKnot(service)(
event({ handle: "tangled.org", repo: "core" }, `/tangled.org/core/${service}${search}`)
);
expect(res.status).toBe(307);
expect(res.headers.get("location")).toBe(
`https://knot1.tangled.sh/${REPO_DID}/${service}${search}`
);
});
it("drops a .git suffix from the repo name", async () => {
await redirectToKnot("info/refs")(
event({ handle: "tangled.org", repo: "core.git" }, "/tangled.org/core.git/info/refs")
);
expect(mocks.resolveRepoByName).toHaveBeenCalledWith(expect.anything(), OWNER_DID, "core");
});
it("follows a did:web knot to its host", async () => {
mocks.resolveRepoByName.mockResolvedValue(repoView("did:web:knot.example.com"));
const res = await redirectToKnot("info/refs")(
event({ handle: "tangled.org", repo: "core" }, "/tangled.org/core/info/refs")
);
expect(res.headers.get("location")).toBe(`https://knot.example.com/${REPO_DID}/info/refs`);
});
it.each([
["the repo does not exist", null],
["the repo has no knot", repoView()]
])("answers 404 when %s", async (_, view) => {
mocks.resolveRepoByName.mockResolvedValue(view);
const handler = redirectToKnot("info/refs");
await expect(
statusOf(
handler(
event({ handle: "tangled.org", repo: "nope" }, "/tangled.org/nope/info/refs")
)
)
).resolves.toBe(404);
});
});
describe("redirectRepoDidToKnot", () => {
it("sends the repo did permalink to its knot", async () => {
const res = await redirectRepoDidToKnot("info/refs")(
event({ handle: REPO_DID }, `/${REPO_DID}/info/refs?service=git-upload-pack`)
);
expect(res.status).toBe(307);
expect(res.headers.get("location")).toBe(
`https://knot1.tangled.sh/${REPO_DID}/info/refs?service=git-upload-pack`
);
});
it("answers 404 for a handle without looking it up", async () => {
const handler = redirectRepoDidToKnot("info/refs");
await expect(
statusOf(handler(event({ handle: "tangled.org" }, "/tangled.org/info/refs")))
).resolves.toBe(404);
expect(mocks.getRepoByRepoDid).not.toHaveBeenCalled();
});
it.each([
[404, "RepoNotFound"],
[503, "Overloaded"]
])("answers %i when looking up the did fails with %s", async (status, error) => {
mocks.getRepoByRepoDid.mockRejectedValue(
new ClientResponseError({ status, data: { error } })
);
const handler = redirectRepoDidToKnot("info/refs");
await expect(
statusOf(handler(event({ handle: OWNER_DID }, `/${OWNER_DID}/info/refs`)))
).resolves.toBe(status);
});
});