442 lines
14 KiB
TypeScript
442 lines
14 KiB
TypeScript
import { file, spawn } from "bun";
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, setDefaultTimeout } from "bun:test";
|
|
import { access, writeFile } from "fs/promises";
|
|
import { bunExe, bunEnv as env, readdirSorted, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness";
|
|
import { join } from "path";
|
|
import {
|
|
dummyAfterAll,
|
|
dummyAfterEach,
|
|
dummyBeforeAll,
|
|
dummyBeforeEach,
|
|
dummyRegistry,
|
|
package_dir,
|
|
requested,
|
|
root_url,
|
|
setHandler,
|
|
} from "./dummy.registry";
|
|
|
|
beforeAll(dummyBeforeAll);
|
|
afterAll(dummyAfterAll);
|
|
|
|
expect.extend({
|
|
toHaveBins,
|
|
toBeValidBin,
|
|
toBeWorkspaceLink,
|
|
});
|
|
|
|
let port: string;
|
|
let add_dir: string;
|
|
setDefaultTimeout(1000 * 60 * 5);
|
|
|
|
beforeAll(() => {
|
|
port = new URL(root_url).port;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
add_dir = tmpdirSync();
|
|
await dummyBeforeEach();
|
|
});
|
|
afterEach(async () => {
|
|
await dummyAfterEach();
|
|
});
|
|
|
|
// Manifest request 302-redirects and the redirect target answers a retryable
|
|
// 500 once. The install retry must restart from the original manifest URL.
|
|
it("retries a manifest whose redirect target 500s once", async () => {
|
|
const urls: string[] = [];
|
|
let redirectTargetHits = 0;
|
|
setHandler(async request => {
|
|
const { pathname } = new URL(request.url);
|
|
urls.push(pathname);
|
|
if (pathname === "/BaR") {
|
|
return new Response(null, { status: 302, headers: { Location: `${root_url}/redirected/BaR` } });
|
|
}
|
|
if (pathname === "/redirected/BaR") {
|
|
if (redirectTargetHits++ === 0) {
|
|
return new Response("transient", { status: 500 });
|
|
}
|
|
return Response.json({
|
|
name: "BaR",
|
|
versions: {
|
|
"0.0.2": { name: "BaR", version: "0.0.2", dist: { tarball: `${root_url}/BaR-0.0.2.tgz` } },
|
|
},
|
|
"dist-tags": { latest: "0.0.2" },
|
|
});
|
|
}
|
|
if (pathname === "/BaR-0.0.2.tgz") {
|
|
return new Response(file(join(import.meta.dir, "bar-0.0.2.tgz")));
|
|
}
|
|
return new Response("unexpected", { status: 404 });
|
|
});
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { BaR: "0.0.2" } }),
|
|
);
|
|
const { stdout, stderr, exited } = spawn({
|
|
cmd: [bunExe(), "install", "--linker=hoisted"],
|
|
cwd: package_dir,
|
|
stdout: "pipe",
|
|
stdin: "pipe",
|
|
stderr: "pipe",
|
|
env,
|
|
});
|
|
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
|
|
expect(err).not.toContain("error:");
|
|
expect(err).toContain("Saved lockfile");
|
|
expect(out).toContain("1 package installed");
|
|
expect(exitCode).toBe(0);
|
|
// The retry restarts from the original manifest URL, so the server sees the
|
|
// whole redirect chain a second time.
|
|
expect(urls).toEqual(["/BaR", "/redirected/BaR", "/BaR", "/redirected/BaR", "/BaR-0.0.2.tgz"]);
|
|
expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({
|
|
name: "bar",
|
|
version: "0.0.2",
|
|
});
|
|
});
|
|
|
|
// A cross-origin redirect strips Authorization from the request (per the fetch
|
|
// spec). The install retry restarts from the original registry URL and must
|
|
// carry the original headers, including Authorization, again.
|
|
it("retries an authorized manifest whose cross-origin redirect target 500s once", async () => {
|
|
const token = "test-registry-token";
|
|
const registryUrls: string[] = [];
|
|
const cdnAuth: (string | null)[] = [];
|
|
let cdnHits = 0;
|
|
// A second server on its own port stands in for the CDN the registry
|
|
// redirects to; a different port makes the redirect cross-origin.
|
|
await using cdn = Bun.serve({
|
|
port: 0,
|
|
fetch(request) {
|
|
if (new URL(request.url).pathname !== "/cdn/BaR") {
|
|
return new Response("unexpected", { status: 404 });
|
|
}
|
|
cdnAuth.push(request.headers.get("authorization"));
|
|
if (cdnHits++ === 0) {
|
|
return new Response("transient", { status: 500 });
|
|
}
|
|
return Response.json({
|
|
name: "BaR",
|
|
versions: {
|
|
"0.0.2": { name: "BaR", version: "0.0.2", dist: { tarball: `${root_url}/BaR-0.0.2.tgz` } },
|
|
},
|
|
"dist-tags": { latest: "0.0.2" },
|
|
});
|
|
},
|
|
});
|
|
setHandler(async request => {
|
|
const { pathname } = new URL(request.url);
|
|
registryUrls.push(pathname);
|
|
if (pathname === "/BaR") {
|
|
// The registry requires the token on every request, including the retry.
|
|
if (request.headers.get("authorization") !== `Bearer ${token}`) {
|
|
return new Response("missing authorization", { status: 401 });
|
|
}
|
|
return new Response(null, {
|
|
status: 302,
|
|
headers: { Location: `http://localhost:${cdn.port}/cdn/BaR` },
|
|
});
|
|
}
|
|
if (pathname === "/BaR-0.0.2.tgz") {
|
|
return new Response(file(join(import.meta.dir, "bar-0.0.2.tgz")));
|
|
}
|
|
return new Response("unexpected", { status: 404 });
|
|
});
|
|
await writeFile(
|
|
join(package_dir, "bunfig.toml"),
|
|
Bun.TOML.stringify({
|
|
install: {
|
|
cache: false,
|
|
registry: { url: `${root_url}/`, token },
|
|
saveTextLockfile: false,
|
|
},
|
|
}),
|
|
);
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { BaR: "0.0.2" } }),
|
|
);
|
|
const { stdout, stderr, exited } = spawn({
|
|
cmd: [bunExe(), "install", "--linker=hoisted"],
|
|
cwd: package_dir,
|
|
stdout: "pipe",
|
|
stdin: "pipe",
|
|
stderr: "pipe",
|
|
env,
|
|
});
|
|
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
|
|
expect(err).not.toContain("error:");
|
|
expect(err).toContain("Saved lockfile");
|
|
expect(out).toContain("1 package installed");
|
|
expect(exitCode).toBe(0);
|
|
// Both registry hits carried the token (the handler 401s otherwise); the
|
|
// cross-origin CDN hops must NOT have (the spec strips it for that hop).
|
|
expect(registryUrls).toEqual(["/BaR", "/BaR", "/BaR-0.0.2.tgz"]);
|
|
expect(cdnAuth).toEqual([null, null]);
|
|
});
|
|
|
|
// Sibling retry site (tarball downloads in runTasks): the tarball URL
|
|
// 302-redirects and the target 500s once before serving the archive.
|
|
it("retries a tarball whose redirect target 500s once", async () => {
|
|
const urls: string[] = [];
|
|
let redirectTargetHits = 0;
|
|
setHandler(async request => {
|
|
const { pathname } = new URL(request.url);
|
|
urls.push(pathname);
|
|
if (pathname === "/BaR") {
|
|
return Response.json({
|
|
name: "BaR",
|
|
versions: {
|
|
"0.0.2": { name: "BaR", version: "0.0.2", dist: { tarball: `${root_url}/BaR-0.0.2.tgz` } },
|
|
},
|
|
"dist-tags": { latest: "0.0.2" },
|
|
});
|
|
}
|
|
if (pathname === "/BaR-0.0.2.tgz") {
|
|
return new Response(null, { status: 302, headers: { Location: `${root_url}/redirected/BaR-0.0.2.tgz` } });
|
|
}
|
|
if (pathname === "/redirected/BaR-0.0.2.tgz") {
|
|
if (redirectTargetHits++ === 0) {
|
|
return new Response("transient", { status: 500 });
|
|
}
|
|
return new Response(file(join(import.meta.dir, "bar-0.0.2.tgz")));
|
|
}
|
|
return new Response("unexpected", { status: 404 });
|
|
});
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { BaR: "0.0.2" } }),
|
|
);
|
|
const { stdout, stderr, exited } = spawn({
|
|
cmd: [bunExe(), "install", "--linker=hoisted"],
|
|
cwd: package_dir,
|
|
stdout: "pipe",
|
|
stdin: "pipe",
|
|
stderr: "pipe",
|
|
env,
|
|
});
|
|
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
|
|
expect(err).not.toContain("error:");
|
|
expect(err).toContain("Saved lockfile");
|
|
expect(out).toContain("1 package installed");
|
|
expect(exitCode).toBe(0);
|
|
expect(urls).toEqual([
|
|
"/BaR",
|
|
"/BaR-0.0.2.tgz",
|
|
"/redirected/BaR-0.0.2.tgz",
|
|
"/BaR-0.0.2.tgz",
|
|
"/redirected/BaR-0.0.2.tgz",
|
|
]);
|
|
expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({
|
|
name: "bar",
|
|
version: "0.0.2",
|
|
});
|
|
});
|
|
|
|
it("retries on 500", async () => {
|
|
const urls: string[] = [];
|
|
setHandler(dummyRegistry(urls, undefined, 4));
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
}),
|
|
);
|
|
const { stdout, stderr, exited } = spawn({
|
|
cmd: [bunExe(), "add", "BaR", "--linker=hoisted"],
|
|
cwd: package_dir,
|
|
stdout: "pipe",
|
|
stdin: "pipe",
|
|
stderr: "pipe",
|
|
env,
|
|
});
|
|
const err = await stderr.text();
|
|
expect(err).not.toContain("error:");
|
|
expect(err).toContain("Saved lockfile");
|
|
const out = await stdout.text();
|
|
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
|
|
expect.stringContaining("bun add v1."),
|
|
"",
|
|
"installed [email protected]",
|
|
"",
|
|
"1 package installed",
|
|
]);
|
|
expect(await exited).toBe(0);
|
|
expect(urls.sort()).toEqual([
|
|
`${root_url}/BaR`,
|
|
`${root_url}/BaR`,
|
|
`${root_url}/BaR`,
|
|
`${root_url}/BaR`,
|
|
`${root_url}/BaR`,
|
|
`${root_url}/BaR`,
|
|
`${root_url}/BaR-0.0.2.tgz`,
|
|
`${root_url}/BaR-0.0.2.tgz`,
|
|
`${root_url}/BaR-0.0.2.tgz`,
|
|
`${root_url}/BaR-0.0.2.tgz`,
|
|
`${root_url}/BaR-0.0.2.tgz`,
|
|
`${root_url}/BaR-0.0.2.tgz`,
|
|
]);
|
|
expect(requested).toBe(12);
|
|
await Promise.all([
|
|
(async () => expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]))(),
|
|
(async () => expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]))(),
|
|
(async () =>
|
|
expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({
|
|
name: "bar",
|
|
version: "0.0.2",
|
|
}))(),
|
|
(async () =>
|
|
expect(await file(join(package_dir, "package.json")).text()).toEqual(
|
|
JSON.stringify(
|
|
{
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: {
|
|
BaR: "^0.0.2",
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
))(),
|
|
async () => await access(join(package_dir, "bun.lockb")),
|
|
]);
|
|
});
|
|
|
|
// A tarball that fails permanently must run its download (and retry cycle)
|
|
// exactly once and be reported as exactly one error. Previously the resolve
|
|
// phase's failure dropped the dedupe entry so the install phase re-ran the
|
|
// entire download: a 500 endpoint saw 12 GETs instead of 6 and the same
|
|
// `error: GET ...` line was printed twice.
|
|
describe.each(["hoisted", "isolated"])("linker=%s", linker => {
|
|
it.each([
|
|
{ status: 404, expectedGets: 1 },
|
|
{ status: 500, expectedGets: 6 },
|
|
])("does not re-download a tarball that already failed with $status", async ({ status, expectedGets }) => {
|
|
const urls: string[] = [];
|
|
setHandler(async request => {
|
|
const { pathname } = new URL(request.url);
|
|
urls.push(pathname);
|
|
if (pathname === "/BaR") {
|
|
return Response.json({
|
|
name: "BaR",
|
|
"dist-tags": { latest: "0.0.2" },
|
|
versions: {
|
|
"0.0.2": { name: "BaR", version: "0.0.2", dist: { tarball: `${root_url}/BaR-0.0.2.tgz` } },
|
|
},
|
|
});
|
|
}
|
|
if (pathname === "/BaR-0.0.2.tgz") {
|
|
return new Response("no", { status });
|
|
}
|
|
return new Response("unexpected", { status: 404 });
|
|
});
|
|
await writeFile(
|
|
join(package_dir, "bunfig.toml"),
|
|
Bun.TOML.stringify({
|
|
install: {
|
|
cache: false,
|
|
registry: `${root_url}/`,
|
|
linker,
|
|
},
|
|
}),
|
|
);
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { BaR: "0.0.2" } }),
|
|
);
|
|
const { stdout, stderr, exited } = spawn({
|
|
cmd: [bunExe(), "install", "--no-progress", "--ignore-scripts"],
|
|
cwd: package_dir,
|
|
stdout: "pipe",
|
|
stdin: "pipe",
|
|
stderr: "pipe",
|
|
env,
|
|
});
|
|
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
|
|
|
|
const tarballGets = urls.filter(u => u === "/BaR-0.0.2.tgz");
|
|
const errorLines = err.split("\n").filter(l => l.startsWith("error:"));
|
|
expect({ tarballGets: tarballGets.length, errorLines }).toEqual({
|
|
tarballGets: expectedGets,
|
|
errorLines: [`error: GET ${root_url}/BaR-0.0.2.tgz - ${status}`],
|
|
});
|
|
expect(out).not.toContain("installed");
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
it("does not re-download an optional dependency's tarball that already failed", async () => {
|
|
const urls: string[] = [];
|
|
setHandler(async request => {
|
|
const { pathname } = new URL(request.url);
|
|
urls.push(pathname);
|
|
if (pathname === "/BaR") {
|
|
return Response.json({
|
|
name: "BaR",
|
|
"dist-tags": { latest: "0.0.2" },
|
|
versions: {
|
|
"0.0.2": { name: "BaR", version: "0.0.2", dist: { tarball: `${root_url}/BaR-0.0.2.tgz` } },
|
|
},
|
|
});
|
|
}
|
|
if (pathname === "/BaR-0.0.2.tgz") return new Response("no", { status: 404 });
|
|
if (pathname === "/baz") {
|
|
return Response.json({
|
|
name: "baz",
|
|
"dist-tags": { latest: "0.0.3" },
|
|
versions: {
|
|
"0.0.3": { name: "baz", version: "0.0.3", dist: { tarball: `${root_url}/baz-0.0.3.tgz` } },
|
|
},
|
|
});
|
|
}
|
|
if (pathname === "/baz-0.0.3.tgz") return new Response(file(join(import.meta.dir, "baz-0.0.3.tgz")));
|
|
return new Response("unexpected", { status: 404 });
|
|
});
|
|
await writeFile(
|
|
join(package_dir, "bunfig.toml"),
|
|
Bun.TOML.stringify({
|
|
install: {
|
|
cache: false,
|
|
registry: `${root_url}/`,
|
|
linker,
|
|
},
|
|
}),
|
|
);
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: { baz: "0.0.3" },
|
|
optionalDependencies: { BaR: "0.0.2" },
|
|
}),
|
|
);
|
|
const { stdout, stderr, exited } = spawn({
|
|
cmd: [bunExe(), "install", "--no-progress", "--ignore-scripts"],
|
|
cwd: package_dir,
|
|
stdout: "pipe",
|
|
stdin: "pipe",
|
|
stderr: "pipe",
|
|
env,
|
|
});
|
|
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
|
|
|
|
const tarballGets = urls.filter(u => u === "/BaR-0.0.2.tgz");
|
|
const warnLines = err.split("\n").filter(l => l.startsWith("warn:"));
|
|
const errorLines = err.split("\n").filter(l => l.startsWith("error:"));
|
|
expect({ tarballGets: tarballGets.length, warnLines, errorLines }).toEqual({
|
|
tarballGets: 1,
|
|
warnLines: [`warn: GET ${root_url}/BaR-0.0.2.tgz - 404`],
|
|
errorLines: [],
|
|
});
|
|
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toMatchObject({
|
|
name: "baz",
|
|
version: "0.0.3",
|
|
});
|
|
if (linker === "hoisted") {
|
|
expect(out).not.toContain("Failed to install");
|
|
expect(exitCode).toBe(0);
|
|
}
|
|
});
|
|
});
|