History

Filter GitHub source entries safely

Rhys Sullivan committed 4 months ago

Commit 9e77e341 parent bb155804 files · +159 −29Browse files

Showing the changes to packages/plugins/apps/src/source/github-source.test.ts (1 of 4 files). Show all

Unified · Split

packages/plugins/apps/src/source/github-source.test.ts+101−12
@@ -8,6 +8,7 @@ import { Effect } from "effect"; · 7 unmodified lines
8import { makeSelfHostAppsRuntime } from "../plugin/self-host-runtime";8import { makeSelfHostAppsRuntime } from "../plugin/self-host-runtime";
9import { makeInMemoryAppsStore, makeTestResolver } from "../testing";9import { makeInMemoryAppsStore, makeTestResolver } from "../testing";
10import { PUBLISH_LIMITS } from "../pipeline/publish";10import { PUBLISH_LIMITS } from "../pipeline/publish";
11import { scopeAddress } from "../seams/scope-address";
11import { fetchGitHubSource, syncGitHubSource } from "./github-source";12import { fetchGitHubSource, syncGitHubSource } from "./github-source";
12 13
13const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect);14const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect);
@@ -34,8 +35,17 @@ const json = (value: unknown, status = 200): Response => · 20 unmodified lines
34 headers: { "content-type": "application/json" },35 headers: { "content-type": "application/json" },
35 });36 });
36 37
38interface GitHubTreeFixtureEntry {
39 readonly path: string;
40 readonly type: string;
41 readonly mode?: string;
42 readonly content?: string;
43 readonly size?: number;
44}
45
37const makeGitHubFetch = (input: {46const makeGitHubFetch = (input: {
38 readonly files: ReadonlyMap<string, string>;47 readonly files: ReadonlyMap<string, string>;
48 readonly entries?: readonly GitHubTreeFixtureEntry[];
39 readonly upstreamSha?: string;49 readonly upstreamSha?: string;
40 readonly treeSha?: string;50 readonly treeSha?: string;
41}) => {51}) => {
@@ -43,9 +53,22 @@ const makeGitHubFetch = (input: { · 1 unmodified line
43 const upstreamSha = input.upstreamSha ?? "commit-1";53 const upstreamSha = input.upstreamSha ?? "commit-1";
44 const treeSha = input.treeSha ?? "tree-1";54 const treeSha = input.treeSha ?? "tree-1";
45 const blobBySha = new Map<string, { path: string; content: string }>();55 const blobBySha = new Map<string, { path: string; content: string }>();
56 const treeEntries: readonly GitHubTreeFixtureEntry[] =
57 input.entries ??
58 [...input.files].map(([path, content]) => ({
59 path,
60 type: "blob",
61 mode: "100644",
62 content,
63 }));
46 let index = 0;64 let index = 0;
47 for (const [path, content] of input.files) {65 const shaByPath = new Map<string, string>();
48 blobBySha.set(`blob-${++index}`, { path, content });66 for (const entry of treeEntries) {
67 if (entry.content !== undefined) {
68 const sha = `blob-${++index}`;
69 shaByPath.set(entry.path, sha);
70 blobBySha.set(sha, { path: entry.path, content: entry.content });
71 }
49 }72 }
50 let blobCalls = 0;73 let blobCalls = 0;
51 const fetch = (async (rawUrl: string) => {74 const fetch = (async (rawUrl: string) => {
@@ -59,12 +82,16 @@ const makeGitHubFetch = (input: { · 7 unmodified lines
59 }82 }
60 if (url.pathname === `${repoPath}/git/trees/${treeSha}`) {83 if (url.pathname === `${repoPath}/git/trees/${treeSha}`) {
61 return json({84 return json({
62 tree: [...blobBySha.entries()].map(([sha, blob]) => ({85 tree: treeEntries.map((entry, entryIndex) => {
63 path: blob.path,86 const sha = shaByPath.get(entry.path) ?? `tree-${entryIndex}`;
64 type: "blob",87 return {
65 sha,88 path: entry.path,
66 size: Buffer.byteLength(blob.content, "utf8"),89 type: entry.type,
67 })),90 mode: entry.mode,
91 sha,
92 size: entry.size ?? Buffer.byteLength(entry.content ?? "", "utf8"),
93 };
94 }),
68 });95 });
69 }96 }
70 const blobPrefix = `${repoPath}/git/blobs/`;97 const blobPrefix = `${repoPath}/git/blobs/`;
@@ -103,10 +130,10 @@ describe("GitHub custom-tools source", () => { · 32 unmodified lines
103 ]);130 ]);
104 const github = makeGitHubFetch({ files, upstreamSha: "commit-a" });131 const github = makeGitHubFetch({ files, upstreamSha: "commit-a" });
105 const snapshot = await run(fetchGitHubSource({ repo: "acme/tools", fetch: github.fetch }));132 const snapshot = await run(fetchGitHubSource({ repo: "acme/tools", fetch: github.fetch }));
106 expect([...snapshot.files.keys()].sort()).toEqual([133 expect([...snapshot.files.keys()].sort()).toEqual(["executor.json", "tools/hello.ts"]);
107 "executor.json",134 expect(snapshot.skipped).toEqual([
108 "tools/hello.ts",135 { path: "workflows/deferred.ts", reason: "not supported yet" },
109 "workflows/deferred.ts",136 { path: "docs/readme.md", reason: "ignored" },
110 ]);137 ]);
111 expect(snapshot.description).toBe("Acme tools");138 expect(snapshot.description).toBe("Acme tools");
112 139
@@ -123,6 +150,7 @@ describe("GitHub custom-tools source", () => { · 10 unmodified lines
123 expect(result.tools).toEqual(["hello"]);150 expect(result.tools).toEqual(["hello"]);
124 expect(result.skipped).toEqual([151 expect(result.skipped).toEqual([
125 { path: "workflows/deferred.ts", reason: "not supported yet" },152 { path: "workflows/deferred.ts", reason: "not supported yet" },
153 { path: "docs/readme.md", reason: "ignored" },
126 ]);154 ]);
127 const descriptor = await run(host.runtime.getDescriptor("githubTools"));155 const descriptor = await run(host.runtime.getDescriptor("githubTools"));
128 expect(descriptor?.description).toBe("Acme tools");156 expect(descriptor?.description).toBe("Acme tools");
@@ -208,4 +236,65 @@ export default defineTool({ description: "bad", input: { type: "object" }, async · 79 unmodified lines
208 expect(result.errors?.[0]?.message).toContain('bare import "lodash" is not allowed');236 expect(result.errors?.[0]?.message).toContain('bare import "lodash" is not allowed');
209 await host.close();237 await host.close();
210 });238 });
239
240 it("skips unsupported tree entries and invalid paths without committing them", async () => {
241 const github = makeGitHubFetch({
242 files: new Map(),
243 entries: [
244 {
245 path: "tools/ok.ts",
246 type: "blob",
247 mode: "100644",
248 content: toolSource("ok"),
249 },
250 {
251 path: "tools/link.ts",
252 type: "blob",
253 mode: "120000",
254 content: "../outside.ts",
255 },
256 {
257 path: "tools/submodule.ts",
258 type: "commit",
259 mode: "160000",
260 },
261 {
262 path: "tools/Bad.Name.ts",
263 type: "blob",
264 mode: "100644",
265 content: toolSource("bad"),
266 },
267 ],
268 upstreamSha: "mixed-sha",
269 });
270 const host = makeRuntime();
271 const result = await run(
272 syncGitHubSource({
273 runtime: host.runtime,
274 scope: "githubTools",
275 repo: "acme/tools",
276 fetch: github.fetch,
277 }),
278 );
279
280 expect(result.status).toBe("published");
281 expect(result.tools).toEqual(["ok"]);
282 expect(result.skipped).toEqual([
283 { path: "tools/link.ts", reason: "unsupported file type" },
284 { path: "tools/submodule.ts", reason: "unsupported file type" },
285 { path: "tools/Bad.Name.ts", reason: "ignored" },
286 ]);
287
288 const descriptor = await run(host.runtime.getDescriptor("githubTools"));
289 expect(descriptor?.tools.map((tool) => tool.name)).toEqual(["ok"]);
290 const scopeStore = await run(
291 host.runtime.deps.artifactStore.forScope(scopeAddress("org", "githubTools")),
292 );
293 const snapshotPaths = await run(scopeStore.list(descriptor!.snapshotId as never));
294 expect(snapshotPaths).toContain("tools/ok.ts");
295 expect(snapshotPaths).not.toContain("tools/link.ts");
296 expect(snapshotPaths).not.toContain("tools/submodule.ts");
297 expect(snapshotPaths).not.toContain("tools/Bad.Name.ts");
298 await host.close();
299 });
211});300});