archive.unit.test.ts
⎇
Raw
1import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2import { mkdirSync, readdirSync, rmSync } from "node:fs";
3import path from "node:path";
4import config from "../src/config.ts";
5
6const TMP = path.resolve(`./data-test-archive-${process.pid}`);
7config.DATA_DIR = TMP;
8
9const { archiveRepo } = await import("../src/services/git.ts");
10
11const REPO = "archivetest";
12
13beforeAll(() => {
14 rmSync(TMP, { recursive: true, force: true });
15 mkdirSync(path.join(TMP, "repos"), { recursive: true });
16
17 const work = path.join(TMP, "work");
18 mkdirSync(work, { recursive: true });
19 const run = (...args: string[]) =>
20 Bun.spawnSync(args, { cwd: work, stdio: ["ignore", "ignore", "ignore"] });
21 run("git", "init", "-q", "-b", "main");
22 run("git", "config", "user.email", "t@example.com");
23 run("git", "config", "user.name", "Test");
24 Bun.write(path.join(work, "hello.txt"), "hello archive\n");
25 run("git", "add", "-A");
26 run("git", "commit", "-q", "-m", "init");
27 Bun.spawnSync(
28 ["git", "clone", "-q", "--bare", work, path.join(TMP, "repos", `${REPO}.git`)],
29 { stdio: ["ignore", "ignore", "ignore"] },
30 );
31});
32
33afterAll(() => {
34 rmSync(TMP, { recursive: true, force: true });
35});
36
37describe("archiveRepo", () => {
38 // The .tar.zst used to be produced by piping git into a `zstd` binary.
39 // It is now compressed in-process, so the container ships without zstd.
40 test("writes all three source archive formats", async () => {
41 const out = path.join(TMP, "out");
42 mkdirSync(out, { recursive: true });
43 await archiveRepo(REPO, "main", "slug", out);
44
45 expect(readdirSync(out).sort()).toEqual([
46 "slug-main.tar.gz",
47 "slug-main.tar.zst",
48 "slug-main.zip",
49 ]);
50 });
51
52 test("the .tar.zst is real zstd and round-trips to the source tar", async () => {
53 const out = path.join(TMP, "out2");
54 mkdirSync(out, { recursive: true });
55 await archiveRepo(REPO, "main", "slug", out);
56
57 const compressed = await Bun.file(
58 path.join(out, "slug-main.tar.zst"),
59 ).bytes();
60 // Zstandard magic number.
61 expect(Array.from(compressed.slice(0, 4))).toEqual([0x28, 0xb5, 0x2f, 0xfd]);
62
63 const tar = Bun.zstdDecompressSync(compressed);
64 // tar stores filenames as plain text in each 512-byte header block.
65 expect(new TextDecoder().decode(tar)).toContain("hello.txt");
66 expect(tar.length % 512).toBe(0);
67 });
68
69 test("a bad ref fails before writing anything", async () => {
70 const out = path.join(TMP, "out3");
71 mkdirSync(out, { recursive: true });
72 // The zip stage throws first, so this never reaches the zst path —
73 // that one is covered separately below.
74 await expect(
75 archiveRepo(REPO, "no-such-ref", "slug", out),
76 ).rejects.toThrow();
77 expect(readdirSync(out)).not.toContain("slug-no-such-ref.tar.zst");
78 });
79
80 test("an abort during the zst stage leaves no partial file", async () => {
81 const out = path.join(TMP, "out4");
82 mkdirSync(out, { recursive: true });
83 const ac = new AbortController();
84 // Abort once the two earlier formats exist, i.e. while the zst stage
85 // is the one in flight. This is the path the inner finally guards:
86 // the sink must be closed before the catch unlinks the file.
87 const done = archiveRepo(REPO, "main", "slug", out, ac.signal);
88 await Bun.sleep(1);
89 ac.abort();
90 await done.catch(() => {});
91
92 expect(readdirSync(out)).not.toContain("slug-main.tar.zst");
93 });
94});
95