@verdaccio/e2e-cli 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/e2e-cli.js +6 -26
- package/build/adapters/adapter.d.ts +1 -0
- package/build/adapters/bun.d.ts +2 -0
- package/build/adapters/deno.d.ts +2 -0
- package/build/adapters/index.d.ts +5 -0
- package/build/adapters/npm.d.ts +2 -0
- package/build/adapters/pnpm.d.ts +2 -0
- package/build/adapters/yarn-modern.d.ts +2 -0
- package/build/cjs/_virtual/_rolldown/runtime.cjs +23 -0
- package/build/cjs/adapters/bun.cjs +64 -0
- package/build/cjs/adapters/bun.cjs.map +1 -0
- package/build/cjs/adapters/deno.cjs +61 -0
- package/build/cjs/adapters/deno.cjs.map +1 -0
- package/build/cjs/adapters/index.cjs +10 -0
- package/build/cjs/adapters/npm.cjs +85 -0
- package/build/cjs/adapters/npm.cjs.map +1 -0
- package/build/cjs/adapters/pnpm.cjs +114 -0
- package/build/cjs/adapters/pnpm.cjs.map +1 -0
- package/build/cjs/adapters/yarn-modern.cjs +207 -0
- package/build/cjs/adapters/yarn-modern.cjs.map +1 -0
- package/build/cjs/index.cjs +186 -11
- package/build/cjs/index.cjs.map +1 -0
- package/build/cjs/reporter.cjs +113 -0
- package/build/cjs/reporter.cjs.map +1 -0
- package/build/cjs/runner.cjs +141 -0
- package/build/cjs/runner.cjs.map +1 -0
- package/build/cjs/scenarios/index.cjs +25 -0
- package/build/cjs/scenarios/index.cjs.map +1 -0
- package/build/cjs/scenarios/install-multiple-deps.cjs +119 -0
- package/build/cjs/scenarios/install-multiple-deps.cjs.map +1 -0
- package/build/cjs/scenarios/metadata.cjs +121 -0
- package/build/cjs/scenarios/metadata.cjs.map +1 -0
- package/build/cjs/scenarios/minimum-release-age.cjs +111 -0
- package/build/cjs/scenarios/minimum-release-age.cjs.map +1 -0
- package/build/cjs/scenarios/search.cjs +200 -0
- package/build/cjs/scenarios/search.cjs.map +1 -0
- package/build/cjs/scenarios/tarballs.cjs +170 -0
- package/build/cjs/scenarios/tarballs.cjs.map +1 -0
- package/build/cjs/scenarios/uplink-failure.cjs +141 -0
- package/build/cjs/scenarios/uplink-failure.cjs.map +1 -0
- package/build/cjs/tests/audit.cjs +60 -0
- package/build/cjs/tests/audit.cjs.map +1 -0
- package/build/cjs/tests/ci.cjs +83 -0
- package/build/cjs/tests/ci.cjs.map +1 -0
- package/build/cjs/tests/deprecate.cjs +108 -0
- package/build/cjs/tests/deprecate.cjs.map +1 -0
- package/build/cjs/tests/dist-tags.cjs +43 -0
- package/build/cjs/tests/dist-tags.cjs.map +1 -0
- package/build/cjs/tests/index.cjs +49 -0
- package/build/cjs/tests/index.cjs.map +1 -0
- package/build/cjs/tests/info.cjs +31 -0
- package/build/cjs/tests/info.cjs.map +1 -0
- package/build/cjs/tests/install.cjs +30 -0
- package/build/cjs/tests/install.cjs.map +1 -0
- package/build/cjs/tests/login.cjs +126 -0
- package/build/cjs/tests/login.cjs.map +1 -0
- package/build/cjs/tests/ping.cjs +26 -0
- package/build/cjs/tests/ping.cjs.map +1 -0
- package/build/cjs/tests/publish.cjs +36 -0
- package/build/cjs/tests/publish.cjs.map +1 -0
- package/build/cjs/tests/search.cjs +23 -0
- package/build/cjs/tests/search.cjs.map +1 -0
- package/build/cjs/tests/unpublish.cjs +40 -0
- package/build/cjs/tests/unpublish.cjs.map +1 -0
- package/build/cjs/utils/e2e-config.cjs +59 -0
- package/build/cjs/utils/e2e-config.cjs.map +1 -0
- package/build/cjs/utils/http-client.cjs +111 -0
- package/build/cjs/utils/http-client.cjs.map +1 -0
- package/build/cjs/utils/info.cjs +25 -0
- package/build/cjs/utils/info.cjs.map +1 -0
- package/build/cjs/utils/mock-uplink.cjs +196 -0
- package/build/cjs/utils/mock-uplink.cjs.map +1 -0
- package/build/cjs/utils/pending-checks.cjs +21 -0
- package/build/cjs/utils/pending-checks.cjs.map +1 -0
- package/build/cjs/utils/process.cjs +96 -0
- package/build/cjs/utils/process.cjs.map +1 -0
- package/build/cjs/utils/project.cjs +55 -0
- package/build/cjs/utils/project.cjs.map +1 -0
- package/build/cjs/utils/publish.cjs +117 -0
- package/build/cjs/utils/publish.cjs.map +1 -0
- package/build/cjs/utils/registry-client.cjs +56 -0
- package/build/cjs/utils/registry-client.cjs.map +1 -0
- package/build/esm/adapters/bun.js +62 -0
- package/build/esm/adapters/bun.js.map +1 -0
- package/build/esm/adapters/deno.js +59 -0
- package/build/esm/adapters/deno.js.map +1 -0
- package/build/esm/adapters/index.js +6 -0
- package/build/esm/adapters/npm.js +83 -0
- package/build/esm/adapters/npm.js.map +1 -0
- package/build/esm/adapters/pnpm.js +112 -0
- package/build/esm/adapters/pnpm.js.map +1 -0
- package/build/esm/adapters/yarn-modern.js +204 -0
- package/build/esm/adapters/yarn-modern.js.map +1 -0
- package/build/esm/index.js +174 -10
- package/build/esm/index.js.map +1 -0
- package/build/esm/reporter.js +108 -0
- package/build/esm/reporter.js.map +1 -0
- package/build/esm/runner.js +138 -0
- package/build/esm/runner.js.map +1 -0
- package/build/esm/scenarios/index.js +19 -0
- package/build/esm/scenarios/index.js.map +1 -0
- package/build/esm/scenarios/install-multiple-deps.js +117 -0
- package/build/esm/scenarios/install-multiple-deps.js.map +1 -0
- package/build/esm/scenarios/metadata.js +119 -0
- package/build/esm/scenarios/metadata.js.map +1 -0
- package/build/esm/scenarios/minimum-release-age.js +108 -0
- package/build/esm/scenarios/minimum-release-age.js.map +1 -0
- package/build/esm/scenarios/search.js +198 -0
- package/build/esm/scenarios/search.js.map +1 -0
- package/build/esm/scenarios/tarballs.js +168 -0
- package/build/esm/scenarios/tarballs.js.map +1 -0
- package/build/esm/scenarios/uplink-failure.js +139 -0
- package/build/esm/scenarios/uplink-failure.js.map +1 -0
- package/build/esm/tests/audit.js +57 -0
- package/build/esm/tests/audit.js.map +1 -0
- package/build/esm/tests/ci.js +80 -0
- package/build/esm/tests/ci.js.map +1 -0
- package/build/esm/tests/deprecate.js +105 -0
- package/build/esm/tests/deprecate.js.map +1 -0
- package/build/esm/tests/dist-tags.js +41 -0
- package/build/esm/tests/dist-tags.js.map +1 -0
- package/build/esm/tests/index.js +37 -0
- package/build/esm/tests/index.js.map +1 -0
- package/build/esm/tests/info.js +29 -0
- package/build/esm/tests/info.js.map +1 -0
- package/build/esm/tests/install.js +28 -0
- package/build/esm/tests/install.js.map +1 -0
- package/build/esm/tests/login.js +122 -0
- package/build/esm/tests/login.js.map +1 -0
- package/build/esm/tests/ping.js +24 -0
- package/build/esm/tests/ping.js.map +1 -0
- package/build/esm/tests/publish.js +34 -0
- package/build/esm/tests/publish.js.map +1 -0
- package/build/esm/tests/search.js +21 -0
- package/build/esm/tests/search.js.map +1 -0
- package/build/esm/tests/unpublish.js +38 -0
- package/build/esm/tests/unpublish.js.map +1 -0
- package/build/esm/utils/e2e-config.js +58 -0
- package/build/esm/utils/e2e-config.js.map +1 -0
- package/build/esm/utils/http-client.js +105 -0
- package/build/esm/utils/http-client.js.map +1 -0
- package/build/esm/utils/info.js +24 -0
- package/build/esm/utils/info.js.map +1 -0
- package/build/esm/utils/mock-uplink.js +194 -0
- package/build/esm/utils/mock-uplink.js.map +1 -0
- package/build/esm/utils/pending-checks.js +21 -0
- package/build/esm/utils/pending-checks.js.map +1 -0
- package/build/esm/utils/process.js +92 -0
- package/build/esm/utils/process.js.map +1 -0
- package/build/esm/utils/project.js +50 -0
- package/build/esm/utils/project.js.map +1 -0
- package/build/esm/utils/publish.js +113 -0
- package/build/esm/utils/publish.js.map +1 -0
- package/build/esm/utils/registry-client.js +53 -0
- package/build/esm/utils/registry-client.js.map +1 -0
- package/build/index.d.ts +6 -15
- package/build/reporter.d.ts +7 -0
- package/build/runner.d.ts +13 -0
- package/build/scenarios/index.d.ts +9 -0
- package/build/scenarios/install-multiple-deps.d.ts +2 -0
- package/build/scenarios/metadata.d.ts +2 -0
- package/build/scenarios/minimum-release-age.d.ts +11 -0
- package/build/scenarios/search.d.ts +2 -0
- package/build/scenarios/tarballs.d.ts +2 -0
- package/build/scenarios/uplink-failure.d.ts +2 -0
- package/build/tests/audit.d.ts +2 -0
- package/build/tests/ci.d.ts +2 -0
- package/build/tests/deprecate.d.ts +2 -0
- package/build/tests/dist-tags.d.ts +2 -0
- package/build/tests/index.d.ts +15 -0
- package/build/tests/info.d.ts +2 -0
- package/build/tests/install.d.ts +2 -0
- package/build/tests/login.d.ts +2 -0
- package/build/tests/ping.d.ts +2 -0
- package/build/tests/publish.d.ts +2 -0
- package/build/tests/search.d.ts +2 -0
- package/build/tests/unpublish.d.ts +2 -0
- package/build/types.d.ts +91 -0
- package/build/utils/e2e-config.d.ts +21 -0
- package/build/utils/http-client.d.ts +49 -0
- package/build/utils/index.d.ts +4 -0
- package/build/utils/info.d.ts +14 -0
- package/build/utils/mock-uplink.d.ts +47 -0
- package/build/utils/pending-checks.d.ts +12 -0
- package/build/utils/process.d.ts +11 -0
- package/build/utils/project.d.ts +6 -0
- package/build/utils/publish.d.ts +17 -0
- package/build/utils/registry-client.d.ts +15 -0
- package/package.json +16 -14
- package/Cargo.lock +0 -1591
- package/Cargo.toml +0 -19
- package/build/bin/verdaccio-e2e +0 -0
- package/scripts/build-js-package.mjs +0 -25
- package/src/e2e_tests/audit.rs +0 -51
- package/src/e2e_tests/ci.rs +0 -32
- package/src/e2e_tests/deprecate.rs +0 -64
- package/src/e2e_tests/dist_tags.rs +0 -56
- package/src/e2e_tests/info.rs +0 -43
- package/src/e2e_tests/install.rs +0 -27
- package/src/e2e_tests/login.rs +0 -64
- package/src/e2e_tests/mod.rs +0 -104
- package/src/e2e_tests/ping.rs +0 -33
- package/src/e2e_tests/publish.rs +0 -68
- package/src/e2e_tests/search.rs +0 -14
- package/src/e2e_tests/unpublish.rs +0 -37
- package/src/main.rs +0 -1411
- package/src/scenarios/install_multiple_deps.rs +0 -99
- package/src/scenarios/minimum_release_age.rs +0 -45
- package/src/scenarios/mod.rs +0 -11
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_process = require("../utils/process.cjs");
|
|
3
|
+
const require_mock_uplink = require("../utils/mock-uplink.cjs");
|
|
4
|
+
const require_pending_checks = require("../utils/pending-checks.cjs");
|
|
5
|
+
const require_publish = require("../utils/publish.cjs");
|
|
6
|
+
let assert = require("assert");
|
|
7
|
+
assert = require_runtime.__toESM(assert, 1);
|
|
8
|
+
//#region src/scenarios/search.ts
|
|
9
|
+
/**
|
|
10
|
+
* Scenario: search
|
|
11
|
+
*
|
|
12
|
+
* HTTP-level battery for `GET /-/v1/search`, pinning the registry.npmjs.org
|
|
13
|
+
* contract (api-documentation `search.yaml` + npm CLI 12 behavior):
|
|
14
|
+
*
|
|
15
|
+
* - result shape npm CLI depends on: `package.name/version/date` and
|
|
16
|
+
* `maintainers` as an array (format-search-stream maps it without a guard)
|
|
17
|
+
* - `total` = total number of matches, not the size of the returned page
|
|
18
|
+
* - `from`/`size` pagination walks the result set without gaps or overlaps
|
|
19
|
+
* - merged local + uplink results are paginated exactly once (the uplink
|
|
20
|
+
* already applies `from` like npmjs does — re-slicing yields empty pages)
|
|
21
|
+
* - missing `text` → 400 (spec: ERR_TEXT_MISSING), not an empty 200
|
|
22
|
+
* - `time` is an ISO 8601 date-time
|
|
23
|
+
* - oversized `size` is clamped, not an error
|
|
24
|
+
* - a real `npm search --json` on top of the raw HTTP checks
|
|
25
|
+
*
|
|
26
|
+
* The uplink sub-test drives the shared MockUplink (which implements
|
|
27
|
+
* /-/v1/search the way npmjs does) and is gated on E2E_UPLINK_PORT plus the
|
|
28
|
+
* registry being started with the config from --print-config.
|
|
29
|
+
*
|
|
30
|
+
* Protocol-level and package-manager-agnostic, so it is gated to the npm
|
|
31
|
+
* adapter to run once per suite.
|
|
32
|
+
*/
|
|
33
|
+
var LOCAL_COUNT = 8;
|
|
34
|
+
var UPLINK_COUNT = 6;
|
|
35
|
+
/**
|
|
36
|
+
* Contract checks that are red against every current registry: real `total`,
|
|
37
|
+
* 400 without `text`, ISO `time`, single pagination of merged local+uplink
|
|
38
|
+
* results, and `package.version` on results. Off by default so CI stays
|
|
39
|
+
* green; enable with E2E_PENDING_CONTRACT_CHECKS=true (and make that the
|
|
40
|
+
* default once the fixes land in verdaccio).
|
|
41
|
+
*/
|
|
42
|
+
var PENDING_CONTRACT_CHECKS_ENABLED = require_pending_checks.pendingContractChecksEnabled("search");
|
|
43
|
+
function uplinkPort() {
|
|
44
|
+
return parseInt(process.env.E2E_UPLINK_PORT || "", 10);
|
|
45
|
+
}
|
|
46
|
+
async function searchV1(registryUrl, query) {
|
|
47
|
+
const url = new URL("/-/v1/search", registryUrl);
|
|
48
|
+
for (const [key, value] of Object.entries(query)) if (value !== void 0) url.searchParams.set(key, String(value));
|
|
49
|
+
require_process.trace("GET %s", url);
|
|
50
|
+
const started = Date.now();
|
|
51
|
+
const response = await fetch(url);
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = JSON.parse(await response.text());
|
|
55
|
+
} catch {
|
|
56
|
+
body = void 0;
|
|
57
|
+
}
|
|
58
|
+
require_process.trace("GET %s → %d in %dms | total=%s objects=%d [%s]", url, response.status, Date.now() - started, body?.total, body?.objects?.length ?? -1, (body?.objects ?? []).map((o) => o?.package?.name).join(", "));
|
|
59
|
+
return {
|
|
60
|
+
status: response.status,
|
|
61
|
+
body
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Page through the result set for `text` and assert the pages are disjoint,
|
|
66
|
+
* respect `size`, and together cover exactly `expectedNames`.
|
|
67
|
+
*/
|
|
68
|
+
async function assertPaginationCovers(registryUrl, text, size, expectedNames) {
|
|
69
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70
|
+
for (let from = 0; from < expectedNames.length; from += size) {
|
|
71
|
+
const remaining = expectedNames.length - from;
|
|
72
|
+
const { status, body } = await searchV1(registryUrl, {
|
|
73
|
+
text,
|
|
74
|
+
size,
|
|
75
|
+
from
|
|
76
|
+
});
|
|
77
|
+
assert.default.strictEqual(status, 200, `Expected 200 for from=${from}`);
|
|
78
|
+
const names = body.objects.map((o) => o.package.name);
|
|
79
|
+
require_process.trace("page from=%d size=%d → %d results: %s", from, size, names.length, names.join(", "));
|
|
80
|
+
assert.default.strictEqual(names.length, Math.min(size, remaining), `Expected ${Math.min(size, remaining)} results at from=${from} (got ${names.length}: ${names.join(", ") || "empty page"})`);
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
assert.default.ok(!seen.has(name), `Duplicate result "${name}" across pages (from=${from})`);
|
|
83
|
+
seen.add(name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const missing = expectedNames.filter((name) => !seen.has(name));
|
|
87
|
+
assert.default.strictEqual(missing.length, 0, `Pagination never returned: ${missing.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
async function testSearch(ctx) {
|
|
90
|
+
const id = ctx.runId;
|
|
91
|
+
const localPrefix = `e2e-srchloc-${id}`;
|
|
92
|
+
const localNames = Array.from({ length: LOCAL_COUNT }, (_, i) => `${localPrefix}-p${i + 1}`);
|
|
93
|
+
const failed = [];
|
|
94
|
+
const check = async (label, fn) => {
|
|
95
|
+
try {
|
|
96
|
+
await ctx.subTest(label, fn);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
require_process.trace("check FAILED: %s — %s", label, err instanceof Error ? err.message : err);
|
|
99
|
+
failed.push(`${label} — ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await ctx.subTest("publish searchable packages", async () => {
|
|
103
|
+
for (const name of localNames) await require_publish.publishLocalPackage(ctx, name, "1.0.0");
|
|
104
|
+
});
|
|
105
|
+
await check("results have the shape npm CLI depends on", async () => {
|
|
106
|
+
const { status, body } = await searchV1(ctx.registryUrl, {
|
|
107
|
+
text: localPrefix,
|
|
108
|
+
size: 250
|
|
109
|
+
});
|
|
110
|
+
assert.default.strictEqual(status, 200, "Expected 200 from /-/v1/search");
|
|
111
|
+
assert.default.ok(Array.isArray(body.objects), "Expected an \"objects\" array");
|
|
112
|
+
require_process.trace("first result object: %j", body.objects[0]);
|
|
113
|
+
assert.default.strictEqual(body.objects.length, LOCAL_COUNT, `Expected ${LOCAL_COUNT} results for ${localPrefix}, got ${body.objects.length}`);
|
|
114
|
+
for (const object of body.objects) {
|
|
115
|
+
assert.default.ok(object.package?.name?.startsWith(localPrefix), "Result outside the query prefix");
|
|
116
|
+
if (PENDING_CONTRACT_CHECKS_ENABLED) assert.default.ok(object.package.version, `Expected a version on ${object.package.name}`);
|
|
117
|
+
assert.default.ok(Array.isArray(object.package.maintainers), `maintainers must be an array on ${object.package.name}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (PENDING_CONTRACT_CHECKS_ENABLED) await check("total is the number of matches, not the page size", async () => {
|
|
121
|
+
const { status, body } = await searchV1(ctx.registryUrl, {
|
|
122
|
+
text: localPrefix,
|
|
123
|
+
size: 3
|
|
124
|
+
});
|
|
125
|
+
assert.default.strictEqual(status, 200, "Expected 200 from /-/v1/search");
|
|
126
|
+
assert.default.strictEqual(body.objects.length, 3, "Expected the page to honor size=3");
|
|
127
|
+
assert.default.strictEqual(body.total, LOCAL_COUNT, `Expected total=${LOCAL_COUNT} (all matches), got ${body.total}`);
|
|
128
|
+
});
|
|
129
|
+
await check("pagination walks local results without gaps", async () => {
|
|
130
|
+
await assertPaginationCovers(ctx.registryUrl, localPrefix, 3, localNames);
|
|
131
|
+
});
|
|
132
|
+
if (PENDING_CONTRACT_CHECKS_ENABLED) {
|
|
133
|
+
await check("missing text returns 400 ERR_TEXT_MISSING", async () => {
|
|
134
|
+
const { status, body } = await searchV1(ctx.registryUrl, {});
|
|
135
|
+
assert.default.strictEqual(status, 400, `Expected 400 without "text", got ${status}`);
|
|
136
|
+
assert.default.ok(body?.error, "Expected an \"error\" field in the 400 body");
|
|
137
|
+
});
|
|
138
|
+
await check("time is an ISO 8601 date-time", async () => {
|
|
139
|
+
const { body } = await searchV1(ctx.registryUrl, {
|
|
140
|
+
text: localPrefix,
|
|
141
|
+
size: 1
|
|
142
|
+
});
|
|
143
|
+
assert.default.ok(body.time, "Expected a \"time\" field");
|
|
144
|
+
assert.default.match(String(body.time), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, `Expected ISO 8601 time, got "${body.time}"`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
await check("oversized size is clamped, not an error", async () => {
|
|
148
|
+
const { status, body } = await searchV1(ctx.registryUrl, {
|
|
149
|
+
text: localPrefix,
|
|
150
|
+
size: 1e5
|
|
151
|
+
});
|
|
152
|
+
assert.default.strictEqual(status, 200, "Expected 200 with an oversized size");
|
|
153
|
+
assert.default.ok(body.objects.length <= 250, "Expected the npmjs size cap (250) to apply");
|
|
154
|
+
});
|
|
155
|
+
await check("npm search finds the published packages", async () => {
|
|
156
|
+
const { tempFolder } = await ctx.adapter.prepareProject(`search-battery-client-${id}`, "1.0.0", ctx.registryUrl, ctx.port, ctx.token);
|
|
157
|
+
const resp = await ctx.adapter.exec({ cwd: tempFolder }, "search", localPrefix, "--json", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
158
|
+
const results = JSON.parse(resp.stdout);
|
|
159
|
+
for (const name of localNames) assert.default.ok(results.find((item) => item.name === name), `Expected ${name} in npm search output`);
|
|
160
|
+
});
|
|
161
|
+
if (PENDING_CONTRACT_CHECKS_ENABLED && Number.isFinite(uplinkPort())) {
|
|
162
|
+
const mock = new require_mock_uplink.MockUplink(uplinkPort());
|
|
163
|
+
const remoteNames = Array.from({ length: UPLINK_COUNT }, (_, i) => `${localPrefix}-r${i + 1}`);
|
|
164
|
+
for (const name of remoteNames) mock.addSearchResult(name);
|
|
165
|
+
await mock.start();
|
|
166
|
+
try {
|
|
167
|
+
await check("merged local+uplink results are paginated once", async () => {
|
|
168
|
+
const allNames = [...localNames, ...remoteNames];
|
|
169
|
+
const { status, body } = await searchV1(ctx.registryUrl, {
|
|
170
|
+
text: localPrefix,
|
|
171
|
+
size: 250
|
|
172
|
+
});
|
|
173
|
+
assert.default.strictEqual(status, 200, "Expected 200 with the uplink up");
|
|
174
|
+
assert.default.strictEqual(body.objects.length, allNames.length, `Expected ${allNames.length} merged results, got ${body.objects.length}`);
|
|
175
|
+
await assertPaginationCovers(ctx.registryUrl, localPrefix, 5, allNames);
|
|
176
|
+
});
|
|
177
|
+
await check("merged total counts local and uplink matches", async () => {
|
|
178
|
+
const { body } = await searchV1(ctx.registryUrl, {
|
|
179
|
+
text: localPrefix,
|
|
180
|
+
size: 5
|
|
181
|
+
});
|
|
182
|
+
assert.default.strictEqual(body.total, 14, `Expected total=14, got ${body.total}`);
|
|
183
|
+
});
|
|
184
|
+
} finally {
|
|
185
|
+
await mock.stop();
|
|
186
|
+
}
|
|
187
|
+
} else require_process.trace("E2E_UPLINK_PORT not set — skipping merged local+uplink pagination sub-tests");
|
|
188
|
+
if (failed.length > 0) throw new Error(`${failed.length} search contract check(s) failed:\n - ${failed.join("\n - ")}`);
|
|
189
|
+
}
|
|
190
|
+
var searchScenario = {
|
|
191
|
+
name: "scenario:search",
|
|
192
|
+
requires: ["publish", "search"],
|
|
193
|
+
timeout: 9e4,
|
|
194
|
+
appliesTo: (adapter) => adapter.type === "npm",
|
|
195
|
+
run: testSearch
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
exports.searchScenario = searchScenario;
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=search.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.cjs","names":[],"sources":["../../../src/scenarios/search.ts"],"sourcesContent":["import assert from 'assert';\n\nimport { TestContext, TestDefinition } from '../types';\nimport { MockUplink } from '../utils/mock-uplink';\nimport { pendingContractChecksEnabled } from '../utils/pending-checks';\nimport { trace } from '../utils/process';\nimport { publishLocalPackage } from '../utils/publish';\n\n/**\n * Scenario: search\n *\n * HTTP-level battery for `GET /-/v1/search`, pinning the registry.npmjs.org\n * contract (api-documentation `search.yaml` + npm CLI 12 behavior):\n *\n * - result shape npm CLI depends on: `package.name/version/date` and\n * `maintainers` as an array (format-search-stream maps it without a guard)\n * - `total` = total number of matches, not the size of the returned page\n * - `from`/`size` pagination walks the result set without gaps or overlaps\n * - merged local + uplink results are paginated exactly once (the uplink\n * already applies `from` like npmjs does — re-slicing yields empty pages)\n * - missing `text` → 400 (spec: ERR_TEXT_MISSING), not an empty 200\n * - `time` is an ISO 8601 date-time\n * - oversized `size` is clamped, not an error\n * - a real `npm search --json` on top of the raw HTTP checks\n *\n * The uplink sub-test drives the shared MockUplink (which implements\n * /-/v1/search the way npmjs does) and is gated on E2E_UPLINK_PORT plus the\n * registry being started with the config from --print-config.\n *\n * Protocol-level and package-manager-agnostic, so it is gated to the npm\n * adapter to run once per suite.\n */\n\nconst LOCAL_COUNT = 8;\nconst UPLINK_COUNT = 6;\n\n/**\n * Contract checks that are red against every current registry: real `total`,\n * 400 without `text`, ISO `time`, single pagination of merged local+uplink\n * results, and `package.version` on results. Off by default so CI stays\n * green; enable with E2E_PENDING_CONTRACT_CHECKS=true (and make that the\n * default once the fixes land in verdaccio).\n */\nconst PENDING_CONTRACT_CHECKS_ENABLED = pendingContractChecksEnabled('search');\n\nfunction uplinkPort(): number {\n return parseInt(process.env.E2E_UPLINK_PORT || '', 10);\n}\n\ntype SearchResponse = {\n status: number;\n body?: any;\n};\n\nasync function searchV1(\n registryUrl: string,\n query: Record<string, string | number | undefined>\n): Promise<SearchResponse> {\n const url = new URL('/-/v1/search', registryUrl);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n }\n trace('GET %s', url);\n const started = Date.now();\n const response = await fetch(url);\n let body: any;\n try {\n body = JSON.parse(await response.text());\n } catch {\n body = undefined;\n }\n trace(\n 'GET %s → %d in %dms | total=%s objects=%d [%s]',\n url,\n response.status,\n Date.now() - started,\n body?.total,\n body?.objects?.length ?? -1,\n (body?.objects ?? []).map((o: any) => o?.package?.name).join(', ')\n );\n return { status: response.status, body };\n}\n\n/**\n * Page through the result set for `text` and assert the pages are disjoint,\n * respect `size`, and together cover exactly `expectedNames`.\n */\nasync function assertPaginationCovers(\n registryUrl: string,\n text: string,\n size: number,\n expectedNames: string[]\n): Promise<void> {\n const seen = new Set<string>();\n for (let from = 0; from < expectedNames.length; from += size) {\n const remaining = expectedNames.length - from;\n const { status, body } = await searchV1(registryUrl, { text, size, from });\n assert.strictEqual(status, 200, `Expected 200 for from=${from}`);\n const names = body.objects.map((o: any) => o.package.name);\n trace('page from=%d size=%d → %d results: %s', from, size, names.length, names.join(', '));\n assert.strictEqual(\n names.length,\n Math.min(size, remaining),\n `Expected ${Math.min(size, remaining)} results at from=${from} (got ${names.length}: ${names.join(', ') || 'empty page'})`\n );\n for (const name of names) {\n assert.ok(!seen.has(name), `Duplicate result \"${name}\" across pages (from=${from})`);\n seen.add(name);\n }\n }\n const missing = expectedNames.filter((name) => !seen.has(name));\n assert.strictEqual(missing.length, 0, `Pagination never returned: ${missing.join(', ')}`);\n}\n\nasync function testSearch(ctx: TestContext): Promise<void> {\n const id = ctx.runId;\n const localPrefix = `e2e-srchloc-${id}`;\n const localNames = Array.from({ length: LOCAL_COUNT }, (_, i) => `${localPrefix}-p${i + 1}`);\n\n // The battery's checks are independent findings against the search contract:\n // run them all and fail at the end, instead of aborting on the first one\n // (ctx.subTest rethrows by design for dependent steps).\n const failed: string[] = [];\n const check = async (label: string, fn: () => Promise<void>): Promise<void> => {\n try {\n await ctx.subTest(label, fn);\n } catch (err) {\n trace('check FAILED: %s — %s', label, err instanceof Error ? err.message : err);\n failed.push(`${label} — ${err instanceof Error ? err.message : String(err)}`);\n }\n };\n\n // Publishing is a real dependency — without it nothing below means anything.\n await ctx.subTest('publish searchable packages', async () => {\n for (const name of localNames) {\n await publishLocalPackage(ctx, name, '1.0.0');\n }\n });\n\n await check('results have the shape npm CLI depends on', async () => {\n const { status, body } = await searchV1(ctx.registryUrl, { text: localPrefix, size: 250 });\n assert.strictEqual(status, 200, 'Expected 200 from /-/v1/search');\n assert.ok(Array.isArray(body.objects), 'Expected an \"objects\" array');\n trace('first result object: %j', body.objects[0]);\n assert.strictEqual(\n body.objects.length,\n LOCAL_COUNT,\n `Expected ${LOCAL_COUNT} results for ${localPrefix}, got ${body.objects.length}`\n );\n for (const object of body.objects) {\n assert.ok(object.package?.name?.startsWith(localPrefix), 'Result outside the query prefix');\n if (PENDING_CONTRACT_CHECKS_ENABLED) {\n // npm CLI reads package.version, but verdaccio 6 omits it in search\n // results — pinned with the rest of the pending contract checks.\n assert.ok(object.package.version, `Expected a version on ${object.package.name}`);\n }\n assert.ok(\n Array.isArray(object.package.maintainers),\n // npm CLI's format-search-stream maps maintainers without a guard\n `maintainers must be an array on ${object.package.name}`\n );\n }\n });\n\n if (PENDING_CONTRACT_CHECKS_ENABLED) {\n // S-3: total must count all matches, not the returned page.\n await check('total is the number of matches, not the page size', async () => {\n const { status, body } = await searchV1(ctx.registryUrl, { text: localPrefix, size: 3 });\n assert.strictEqual(status, 200, 'Expected 200 from /-/v1/search');\n assert.strictEqual(body.objects.length, 3, 'Expected the page to honor size=3');\n assert.strictEqual(\n body.total,\n LOCAL_COUNT,\n `Expected total=${LOCAL_COUNT} (all matches), got ${body.total}`\n );\n });\n }\n\n await check('pagination walks local results without gaps', async () => {\n await assertPaginationCovers(ctx.registryUrl, localPrefix, 3, localNames);\n });\n\n if (PENDING_CONTRACT_CHECKS_ENABLED) {\n // S-4: the spec documents 400 ERR_TEXT_MISSING when text is absent.\n await check('missing text returns 400 ERR_TEXT_MISSING', async () => {\n const { status, body } = await searchV1(ctx.registryUrl, {});\n assert.strictEqual(status, 400, `Expected 400 without \"text\", got ${status}`);\n assert.ok(body?.error, 'Expected an \"error\" field in the 400 body');\n });\n\n // S-5: the spec documents an ISO 8601 date-time, not toUTCString().\n await check('time is an ISO 8601 date-time', async () => {\n const { body } = await searchV1(ctx.registryUrl, { text: localPrefix, size: 1 });\n assert.ok(body.time, 'Expected a \"time\" field');\n assert.match(\n String(body.time),\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/,\n `Expected ISO 8601 time, got \"${body.time}\"`\n );\n });\n }\n\n await check('oversized size is clamped, not an error', async () => {\n const { status, body } = await searchV1(ctx.registryUrl, { text: localPrefix, size: 100000 });\n assert.strictEqual(status, 200, 'Expected 200 with an oversized size');\n assert.ok(body.objects.length <= 250, 'Expected the npmjs size cap (250) to apply');\n });\n\n await check('npm search finds the published packages', async () => {\n const { tempFolder } = await ctx.adapter.prepareProject(\n `search-battery-client-${id}`,\n '1.0.0',\n ctx.registryUrl,\n ctx.port,\n ctx.token\n );\n const resp = await ctx.adapter.exec(\n { cwd: tempFolder },\n 'search',\n localPrefix,\n '--json',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n const results = JSON.parse(resp.stdout);\n for (const name of localNames) {\n assert.ok(\n results.find((item: any) => item.name === name),\n `Expected ${name} in npm search output`\n );\n }\n });\n\n // S-1: merged local + uplink pagination — the case where double slicing\n // shows up: the uplink already returns the offset page (like npmjs), so\n // slicing again in the registry produces empty or shifted pages.\n if (PENDING_CONTRACT_CHECKS_ENABLED && Number.isFinite(uplinkPort())) {\n const mock = new MockUplink(uplinkPort());\n const remoteNames = Array.from({ length: UPLINK_COUNT }, (_, i) => `${localPrefix}-r${i + 1}`);\n for (const name of remoteNames) {\n mock.addSearchResult(name);\n }\n await mock.start();\n try {\n await check('merged local+uplink results are paginated once', async () => {\n const allNames = [...localNames, ...remoteNames];\n const { status, body } = await searchV1(ctx.registryUrl, { text: localPrefix, size: 250 });\n assert.strictEqual(status, 200, 'Expected 200 with the uplink up');\n assert.strictEqual(\n body.objects.length,\n allNames.length,\n `Expected ${allNames.length} merged results, got ${body.objects.length}`\n );\n await assertPaginationCovers(ctx.registryUrl, localPrefix, 5, allNames);\n });\n\n await check('merged total counts local and uplink matches', async () => {\n const { body } = await searchV1(ctx.registryUrl, { text: localPrefix, size: 5 });\n assert.strictEqual(\n body.total,\n LOCAL_COUNT + UPLINK_COUNT,\n `Expected total=${LOCAL_COUNT + UPLINK_COUNT}, got ${body.total}`\n );\n });\n } finally {\n await mock.stop();\n }\n } else {\n trace('E2E_UPLINK_PORT not set — skipping merged local+uplink pagination sub-tests');\n }\n\n if (failed.length > 0) {\n throw new Error(\n `${failed.length} search contract check(s) failed:\\n - ${failed.join('\\n - ')}`\n );\n }\n}\n\nexport const searchScenario: TestDefinition = {\n name: 'scenario:search',\n requires: ['publish', 'search'],\n timeout: 90_000,\n // Protocol-level battery — run once per suite via the npm adapter.\n appliesTo: (adapter) => adapter.type === 'npm',\n run: testSearch,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_process = require("../utils/process.cjs");
|
|
3
|
+
const require_http_client = require("../utils/http-client.cjs");
|
|
4
|
+
const require_pending_checks = require("../utils/pending-checks.cjs");
|
|
5
|
+
let path = require("path");
|
|
6
|
+
let fs_promises = require("fs/promises");
|
|
7
|
+
let assert = require("assert");
|
|
8
|
+
assert = require_runtime.__toESM(assert, 1);
|
|
9
|
+
let crypto = require("crypto");
|
|
10
|
+
//#region src/scenarios/tarballs.ts
|
|
11
|
+
/**
|
|
12
|
+
* Scenario: tarballs
|
|
13
|
+
*
|
|
14
|
+
* HTTP-level battery for the tarball endpoints. Publishes a large (~30 MB by
|
|
15
|
+
* default, incompressible) package through the real package manager, then
|
|
16
|
+
* exercises the raw registry protocol with fetch():
|
|
17
|
+
*
|
|
18
|
+
* - full download with Content-Length + shasum/integrity verification
|
|
19
|
+
* - client aborts mid-download (repeatedly) without breaking the registry
|
|
20
|
+
* - concurrent downloads of the same large tarball
|
|
21
|
+
* - 404 behavior for missing packages / versions / mismatched filenames
|
|
22
|
+
* - scoped packages via the npm `%2f`-encoded URL form
|
|
23
|
+
* - end-to-end install of the large package through the package manager
|
|
24
|
+
*
|
|
25
|
+
* The registry must accept large publishes — Verdaccio's default
|
|
26
|
+
* `max_body_size` is 10mb, so the harness config needs e.g. `max_body_size: 100mb`
|
|
27
|
+
* (see `--print-config`). The payload size can be tuned with
|
|
28
|
+
* E2E_LARGE_TARBALL_MB (default 30).
|
|
29
|
+
*
|
|
30
|
+
* Protocol-level and package-manager-agnostic, so it is gated to the npm
|
|
31
|
+
* adapter to run once per suite.
|
|
32
|
+
*/
|
|
33
|
+
var LARGE_MB = parseInt(process.env.E2E_LARGE_TARBALL_MB || "30", 10);
|
|
34
|
+
var CONCURRENT_DOWNLOADS = 8;
|
|
35
|
+
/**
|
|
36
|
+
* Contract checks that are red against current registries: tarballs are
|
|
37
|
+
* streamed chunked with no Content-Length, and compression() re-gzips the
|
|
38
|
+
* already-compressed .tgz for gzip-accepting clients. Off by default so CI
|
|
39
|
+
* stays green; enable with E2E_PENDING_CONTRACT_CHECKS=true (and make that
|
|
40
|
+
* the default once the fixes land in a published verdaccio).
|
|
41
|
+
*/
|
|
42
|
+
var PENDING_CONTRACT_CHECKS_ENABLED = require_pending_checks.pendingContractChecksEnabled("tarballs");
|
|
43
|
+
async function publishWithPayload(ctx, pkgName, version, payloadBytes) {
|
|
44
|
+
const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token);
|
|
45
|
+
if (payloadBytes) await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "payload.bin"), (0, crypto.randomBytes)(payloadBytes));
|
|
46
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
47
|
+
require_process.trace("published %s@%s (payload: %d bytes)", pkgName, version, payloadBytes ?? 0);
|
|
48
|
+
}
|
|
49
|
+
async function getDist(ctx, pkgName, version) {
|
|
50
|
+
const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, pkgName);
|
|
51
|
+
assert.default.strictEqual(status, 200, `Expected 200 fetching packument for ${pkgName}`);
|
|
52
|
+
const dist = body?.versions?.[version]?.dist;
|
|
53
|
+
assert.default.ok(dist?.tarball, `Expected dist.tarball for ${pkgName}@${version}`);
|
|
54
|
+
return dist;
|
|
55
|
+
}
|
|
56
|
+
async function testTarballs(ctx) {
|
|
57
|
+
const id = ctx.runId;
|
|
58
|
+
const largePkg = `e2e-tarball-large-${id}`;
|
|
59
|
+
const largeBytes = LARGE_MB * 1024 * 1024;
|
|
60
|
+
let largeTarballUrl = "";
|
|
61
|
+
let largeSha1 = "";
|
|
62
|
+
await ctx.subTest(`publish large package (~${LARGE_MB} MB)`, async () => {
|
|
63
|
+
await publishWithPayload(ctx, largePkg, "1.0.0", largeBytes);
|
|
64
|
+
});
|
|
65
|
+
await ctx.subTest("download large tarball and verify integrity", async () => {
|
|
66
|
+
const dist = await getDist(ctx, largePkg, "1.0.0");
|
|
67
|
+
largeTarballUrl = dist.tarball;
|
|
68
|
+
const download = await require_http_client.downloadTarball(dist.tarball);
|
|
69
|
+
require_process.trace("large tarball %s → %d | bytes=%d content-length=%s content-encoding=%s transfer-encoding=%s", dist.tarball, download.status, download.bytes, download.headers.get("content-length"), download.headers.get("content-encoding"), download.headers.get("transfer-encoding"));
|
|
70
|
+
require_process.trace("advertised dist: shasum=%s integrity=%s", dist.shasum, dist.integrity);
|
|
71
|
+
assert.default.strictEqual(download.status, 200, `Expected 200 downloading ${dist.tarball}`);
|
|
72
|
+
assert.default.ok(download.bytes >= largeBytes, `Expected tarball >= ${largeBytes} bytes (incompressible payload), got ${download.bytes}`);
|
|
73
|
+
const contentLength = download.headers.get("content-length");
|
|
74
|
+
if (contentLength) assert.default.strictEqual(parseInt(contentLength, 10), download.bytes, `Content-Length (${contentLength}) does not match received bytes (${download.bytes})`);
|
|
75
|
+
assert.default.ok(dist.shasum, "Expected dist.shasum on the published version");
|
|
76
|
+
assert.default.strictEqual(download.sha1, dist.shasum, "shasum mismatch on downloaded tarball");
|
|
77
|
+
assert.default.ok(dist.integrity, "Expected dist.integrity on the published version");
|
|
78
|
+
assert.default.strictEqual(download.integrity, dist.integrity, "integrity mismatch on downloaded tarball");
|
|
79
|
+
largeSha1 = download.sha1;
|
|
80
|
+
});
|
|
81
|
+
if (PENDING_CONTRACT_CHECKS_ENABLED) {
|
|
82
|
+
await ctx.subTest("tarball response advertises Content-Length", async () => {
|
|
83
|
+
const response = await fetch(largeTarballUrl, { headers: { "accept-encoding": "identity" } });
|
|
84
|
+
await response.body?.cancel();
|
|
85
|
+
const contentLength = response.headers.get("content-length");
|
|
86
|
+
assert.default.ok(contentLength, "Expected Content-Length on an identity-encoded tarball response");
|
|
87
|
+
assert.default.ok(parseInt(contentLength, 10) >= largeBytes, `Content-Length (${contentLength}) smaller than the payload (${largeBytes})`);
|
|
88
|
+
});
|
|
89
|
+
await ctx.subTest("tarball is not re-compressed for gzip-accepting clients", async () => {
|
|
90
|
+
const response = await fetch(largeTarballUrl, { headers: { "accept-encoding": "gzip" } });
|
|
91
|
+
await response.body?.cancel();
|
|
92
|
+
assert.default.strictEqual(response.headers.get("content-encoding"), null, "Registry re-compresses the already-gzipped .tgz (wasted CPU on every download)");
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
await ctx.subTest("abort mid-download is handled gracefully", async () => {
|
|
96
|
+
const abortThresholds = [
|
|
97
|
+
1,
|
|
98
|
+
Math.floor(largeBytes * .25),
|
|
99
|
+
Math.floor(largeBytes * .5)
|
|
100
|
+
];
|
|
101
|
+
for (const threshold of abortThresholds) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
let aborted = false;
|
|
104
|
+
try {
|
|
105
|
+
await require_http_client.downloadTarball(largeTarballUrl, {
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
onChunk: (bytes) => {
|
|
108
|
+
if (bytes >= threshold) controller.abort();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
aborted = true;
|
|
113
|
+
require_process.trace("download aborted at >=%d bytes as expected: %s", threshold, err instanceof Error ? err.message : err);
|
|
114
|
+
}
|
|
115
|
+
assert.default.ok(aborted, `Expected download to abort mid-stream (threshold ${threshold} bytes)`);
|
|
116
|
+
}
|
|
117
|
+
const ping = await fetch(`${ctx.registryUrl}/-/ping`);
|
|
118
|
+
require_process.trace("ping after aborts → %d", ping.status);
|
|
119
|
+
assert.default.strictEqual(ping.status, 200, "Registry did not respond to ping after aborts");
|
|
120
|
+
const download = await require_http_client.downloadTarball(largeTarballUrl);
|
|
121
|
+
require_process.trace("full download after aborts → %d bytes=%d", download.status, download.bytes);
|
|
122
|
+
assert.default.strictEqual(download.status, 200, "Full download after aborts failed");
|
|
123
|
+
assert.default.strictEqual(download.sha1, largeSha1, "Tarball corrupted after aborted downloads");
|
|
124
|
+
});
|
|
125
|
+
await ctx.subTest(`${CONCURRENT_DOWNLOADS} concurrent downloads stay consistent`, async () => {
|
|
126
|
+
const downloads = await Promise.all(Array.from({ length: CONCURRENT_DOWNLOADS }, () => require_http_client.downloadTarball(largeTarballUrl)));
|
|
127
|
+
for (const download of downloads) {
|
|
128
|
+
assert.default.strictEqual(download.status, 200, "Concurrent download failed");
|
|
129
|
+
assert.default.strictEqual(download.sha1, largeSha1, "Concurrent download corrupted");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
await ctx.subTest("missing tarballs return 404", async () => {
|
|
133
|
+
const cases = [
|
|
134
|
+
`${require_http_client.packumentUrl(ctx.registryUrl, largePkg)}/-/${largePkg}-9.9.9.tgz`,
|
|
135
|
+
`${require_http_client.packumentUrl(ctx.registryUrl, `does-not-exist-${id}`)}/-/does-not-exist-${id}-1.0.0.tgz`,
|
|
136
|
+
`${require_http_client.packumentUrl(ctx.registryUrl, largePkg)}/-/some-other-file-1.0.0.tgz`
|
|
137
|
+
];
|
|
138
|
+
for (const url of cases) {
|
|
139
|
+
const response = await fetch(url);
|
|
140
|
+
const responseBody = await response.text();
|
|
141
|
+
require_process.trace("404 case %s → %d | body=%s", url, response.status, responseBody.slice(0, 200));
|
|
142
|
+
assert.default.strictEqual(response.status, 404, `Expected 404 for ${url}, got ${response.status}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
await ctx.subTest("scoped package tarball via %2f-encoded URL", async () => {
|
|
146
|
+
const scopedPkg = `@verdaccio/e2e-tarball-${id}`;
|
|
147
|
+
await publishWithPayload(ctx, scopedPkg, "1.0.0");
|
|
148
|
+
const dist = await getDist(ctx, scopedPkg, "1.0.0");
|
|
149
|
+
const download = await require_http_client.downloadTarball(dist.tarball);
|
|
150
|
+
assert.default.strictEqual(download.status, 200, `Expected 200 downloading ${dist.tarball}`);
|
|
151
|
+
assert.default.ok(download.bytes > 0, "Scoped tarball is empty");
|
|
152
|
+
assert.default.ok(dist.shasum, "Expected dist.shasum on the scoped version");
|
|
153
|
+
assert.default.strictEqual(download.sha1, dist.shasum, "Scoped tarball shasum mismatch");
|
|
154
|
+
});
|
|
155
|
+
await ctx.subTest("package manager installs the large package", async () => {
|
|
156
|
+
const { tempFolder } = await ctx.adapter.prepareProject(`e2e-tarball-consumer-${id}`, "1.0.0", ctx.registryUrl, ctx.port, ctx.token, { [largePkg]: "1.0.0" });
|
|
157
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "install", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
var tarballsScenario = {
|
|
161
|
+
name: "scenario:tarballs",
|
|
162
|
+
requires: ["publish", "install"],
|
|
163
|
+
appliesTo: (adapter) => adapter.type === "npm",
|
|
164
|
+
timeout: 18e4,
|
|
165
|
+
run: testTarballs
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
exports.tarballsScenario = tarballsScenario;
|
|
169
|
+
|
|
170
|
+
//# sourceMappingURL=tarballs.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tarballs.cjs","names":[],"sources":["../../../src/scenarios/tarballs.ts"],"sourcesContent":["import assert from 'assert';\nimport { randomBytes } from 'crypto';\nimport { writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nimport { TestContext, TestDefinition } from '../types';\nimport { downloadTarball, fetchPackument, packumentUrl } from '../utils/http-client';\nimport { pendingContractChecksEnabled } from '../utils/pending-checks';\nimport { trace } from '../utils/process';\n\n/**\n * Scenario: tarballs\n *\n * HTTP-level battery for the tarball endpoints. Publishes a large (~30 MB by\n * default, incompressible) package through the real package manager, then\n * exercises the raw registry protocol with fetch():\n *\n * - full download with Content-Length + shasum/integrity verification\n * - client aborts mid-download (repeatedly) without breaking the registry\n * - concurrent downloads of the same large tarball\n * - 404 behavior for missing packages / versions / mismatched filenames\n * - scoped packages via the npm `%2f`-encoded URL form\n * - end-to-end install of the large package through the package manager\n *\n * The registry must accept large publishes — Verdaccio's default\n * `max_body_size` is 10mb, so the harness config needs e.g. `max_body_size: 100mb`\n * (see `--print-config`). The payload size can be tuned with\n * E2E_LARGE_TARBALL_MB (default 30).\n *\n * Protocol-level and package-manager-agnostic, so it is gated to the npm\n * adapter to run once per suite.\n */\n\nconst LARGE_MB = parseInt(process.env.E2E_LARGE_TARBALL_MB || '30', 10);\nconst CONCURRENT_DOWNLOADS = 8;\n\n/**\n * Contract checks that are red against current registries: tarballs are\n * streamed chunked with no Content-Length, and compression() re-gzips the\n * already-compressed .tgz for gzip-accepting clients. Off by default so CI\n * stays green; enable with E2E_PENDING_CONTRACT_CHECKS=true (and make that\n * the default once the fixes land in a published verdaccio).\n */\nconst PENDING_CONTRACT_CHECKS_ENABLED = pendingContractChecksEnabled('tarballs');\n\nasync function publishWithPayload(\n ctx: TestContext,\n pkgName: string,\n version: string,\n payloadBytes?: number\n): Promise<void> {\n const { tempFolder } = await ctx.adapter.prepareProject(\n pkgName,\n version,\n ctx.registryUrl,\n ctx.port,\n ctx.token\n );\n if (payloadBytes) {\n // Random bytes are incompressible, so the gzipped tarball keeps ~this size.\n await writeFile(join(tempFolder, 'payload.bin'), randomBytes(payloadBytes));\n }\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'publish',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n trace('published %s@%s (payload: %d bytes)', pkgName, version, payloadBytes ?? 0);\n}\n\nasync function getDist(\n ctx: TestContext,\n pkgName: string,\n version: string\n): Promise<{ tarball: string; shasum?: string; integrity?: string }> {\n const { status, body } = await fetchPackument(ctx.registryUrl, pkgName);\n assert.strictEqual(status, 200, `Expected 200 fetching packument for ${pkgName}`);\n const dist = body?.versions?.[version]?.dist;\n assert.ok(dist?.tarball, `Expected dist.tarball for ${pkgName}@${version}`);\n return dist;\n}\n\nasync function testTarballs(ctx: TestContext): Promise<void> {\n const id = ctx.runId;\n const largePkg = `e2e-tarball-large-${id}`;\n const largeBytes = LARGE_MB * 1024 * 1024;\n let largeTarballUrl = '';\n let largeSha1 = '';\n\n await ctx.subTest(`publish large package (~${LARGE_MB} MB)`, async () => {\n await publishWithPayload(ctx, largePkg, '1.0.0', largeBytes);\n });\n\n await ctx.subTest('download large tarball and verify integrity', async () => {\n const dist = await getDist(ctx, largePkg, '1.0.0');\n largeTarballUrl = dist.tarball;\n\n const download = await downloadTarball(dist.tarball);\n trace(\n 'large tarball %s → %d | bytes=%d content-length=%s content-encoding=%s transfer-encoding=%s',\n dist.tarball,\n download.status,\n download.bytes,\n download.headers.get('content-length'),\n download.headers.get('content-encoding'),\n download.headers.get('transfer-encoding')\n );\n trace('advertised dist: shasum=%s integrity=%s', dist.shasum, dist.integrity);\n assert.strictEqual(download.status, 200, `Expected 200 downloading ${dist.tarball}`);\n assert.ok(\n download.bytes >= largeBytes,\n `Expected tarball >= ${largeBytes} bytes (incompressible payload), got ${download.bytes}`\n );\n // When Content-Length is present it must match; its *presence* is pinned\n // by the gated contract check below (Verdaccio streams chunked today).\n const contentLength = download.headers.get('content-length');\n if (contentLength) {\n assert.strictEqual(\n parseInt(contentLength, 10),\n download.bytes,\n `Content-Length (${contentLength}) does not match received bytes (${download.bytes})`\n );\n }\n // A locally published version must always advertise its integrity — the\n // absence of these fields IS a bug, so no conditional soft-skips.\n assert.ok(dist.shasum, 'Expected dist.shasum on the published version');\n assert.strictEqual(download.sha1, dist.shasum, 'shasum mismatch on downloaded tarball');\n assert.ok(dist.integrity, 'Expected dist.integrity on the published version');\n assert.strictEqual(\n download.integrity,\n dist.integrity,\n 'integrity mismatch on downloaded tarball'\n );\n largeSha1 = download.sha1;\n });\n\n if (PENDING_CONTRACT_CHECKS_ENABLED) {\n await ctx.subTest('tarball response advertises Content-Length', async () => {\n const response = await fetch(largeTarballUrl, {\n headers: { 'accept-encoding': 'identity' },\n });\n await response.body?.cancel();\n const contentLength = response.headers.get('content-length');\n assert.ok(contentLength, 'Expected Content-Length on an identity-encoded tarball response');\n assert.ok(\n parseInt(contentLength as string, 10) >= largeBytes,\n `Content-Length (${contentLength}) smaller than the payload (${largeBytes})`\n );\n });\n\n await ctx.subTest('tarball is not re-compressed for gzip-accepting clients', async () => {\n const response = await fetch(largeTarballUrl, { headers: { 'accept-encoding': 'gzip' } });\n await response.body?.cancel();\n assert.strictEqual(\n response.headers.get('content-encoding'),\n null,\n 'Registry re-compresses the already-gzipped .tgz (wasted CPU on every download)'\n );\n });\n }\n\n await ctx.subTest('abort mid-download is handled gracefully', async () => {\n // Abort at different depths into the stream: the first chunk exercises the\n // early-teardown path, the deeper thresholds exercise aborts while the\n // registry is mid-flight with backpressure on a large body.\n const abortThresholds = [1, Math.floor(largeBytes * 0.25), Math.floor(largeBytes * 0.5)];\n for (const threshold of abortThresholds) {\n const controller = new AbortController();\n let aborted = false;\n try {\n await downloadTarball(largeTarballUrl, {\n signal: controller.signal,\n onChunk: (bytes) => {\n if (bytes >= threshold) {\n controller.abort();\n }\n },\n });\n } catch (err) {\n aborted = true;\n trace(\n 'download aborted at >=%d bytes as expected: %s',\n threshold,\n err instanceof Error ? err.message : err\n );\n }\n assert.ok(aborted, `Expected download to abort mid-stream (threshold ${threshold} bytes)`);\n }\n\n // The registry must survive the aborts: ping + a full download still work.\n const ping = await fetch(`${ctx.registryUrl}/-/ping`);\n trace('ping after aborts → %d', ping.status);\n assert.strictEqual(ping.status, 200, 'Registry did not respond to ping after aborts');\n const download = await downloadTarball(largeTarballUrl);\n trace('full download after aborts → %d bytes=%d', download.status, download.bytes);\n assert.strictEqual(download.status, 200, 'Full download after aborts failed');\n assert.strictEqual(download.sha1, largeSha1, 'Tarball corrupted after aborted downloads');\n });\n\n await ctx.subTest(`${CONCURRENT_DOWNLOADS} concurrent downloads stay consistent`, async () => {\n const downloads = await Promise.all(\n Array.from({ length: CONCURRENT_DOWNLOADS }, () => downloadTarball(largeTarballUrl))\n );\n for (const download of downloads) {\n assert.strictEqual(download.status, 200, 'Concurrent download failed');\n assert.strictEqual(download.sha1, largeSha1, 'Concurrent download corrupted');\n }\n });\n\n await ctx.subTest('missing tarballs return 404', async () => {\n const cases = [\n // existing package, nonexistent version\n `${packumentUrl(ctx.registryUrl, largePkg)}/-/${largePkg}-9.9.9.tgz`,\n // nonexistent package\n `${packumentUrl(ctx.registryUrl, `does-not-exist-${id}`)}/-/does-not-exist-${id}-1.0.0.tgz`,\n // existing package, filename that doesn't belong to it\n `${packumentUrl(ctx.registryUrl, largePkg)}/-/some-other-file-1.0.0.tgz`,\n ];\n for (const url of cases) {\n const response = await fetch(url);\n // drain the body so sockets are released\n const responseBody = await response.text();\n trace('404 case %s → %d | body=%s', url, response.status, responseBody.slice(0, 200));\n assert.strictEqual(response.status, 404, `Expected 404 for ${url}, got ${response.status}`);\n }\n });\n\n await ctx.subTest('scoped package tarball via %2f-encoded URL', async () => {\n const scopedPkg = `@verdaccio/e2e-tarball-${id}`;\n await publishWithPayload(ctx, scopedPkg, '1.0.0');\n const dist = await getDist(ctx, scopedPkg, '1.0.0');\n const download = await downloadTarball(dist.tarball);\n assert.strictEqual(download.status, 200, `Expected 200 downloading ${dist.tarball}`);\n assert.ok(download.bytes > 0, 'Scoped tarball is empty');\n assert.ok(dist.shasum, 'Expected dist.shasum on the scoped version');\n assert.strictEqual(download.sha1, dist.shasum, 'Scoped tarball shasum mismatch');\n });\n\n await ctx.subTest('package manager installs the large package', async () => {\n const { tempFolder } = await ctx.adapter.prepareProject(\n `e2e-tarball-consumer-${id}`,\n '1.0.0',\n ctx.registryUrl,\n ctx.port,\n ctx.token,\n { [largePkg]: '1.0.0' }\n );\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'install',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n });\n}\n\nexport const tarballsScenario: TestDefinition = {\n name: 'scenario:tarballs',\n requires: ['publish', 'install'],\n // Protocol-level battery — run once per suite via the npm adapter.\n appliesTo: (adapter) => adapter.type === 'npm',\n timeout: 180_000,\n run: testTarballs,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_process = require("../utils/process.cjs");
|
|
3
|
+
const require_http_client = require("../utils/http-client.cjs");
|
|
4
|
+
const require_mock_uplink = require("../utils/mock-uplink.cjs");
|
|
5
|
+
let assert = require("assert");
|
|
6
|
+
assert = require_runtime.__toESM(assert, 1);
|
|
7
|
+
//#region src/scenarios/uplink-failure.ts
|
|
8
|
+
/**
|
|
9
|
+
* Scenario: uplink-failure
|
|
10
|
+
*
|
|
11
|
+
* Verifies how the registry behaves when its uplink misbehaves. Requires the
|
|
12
|
+
* registry to be configured with an uplink pointing at the mock uplink port
|
|
13
|
+
* and a package pattern proxied to it (see `--print-config`):
|
|
14
|
+
*
|
|
15
|
+
* uplinks:
|
|
16
|
+
* e2emock:
|
|
17
|
+
* url: http://localhost:<E2E_UPLINK_PORT>/
|
|
18
|
+
* timeout: 3s
|
|
19
|
+
* max_fails: 100
|
|
20
|
+
* packages:
|
|
21
|
+
* 'e2e-uplink-*':
|
|
22
|
+
* access: $all
|
|
23
|
+
* proxy: e2emock
|
|
24
|
+
*
|
|
25
|
+
* The scenario starts its own HTTP server on that port and flips it between
|
|
26
|
+
* modes:
|
|
27
|
+
* - healthy: packument + tarball proxied through, tarball URL rewritten
|
|
28
|
+
* - drop-mid-stream: uplink kills the socket halfway through a tarball —
|
|
29
|
+
* the client must not receive a "successful" corrupt download, and the
|
|
30
|
+
* tarball must be intact on retry (no poisoned cache)
|
|
31
|
+
* - down: previously fetched packages still served from cache; unknown
|
|
32
|
+
* packages fail cleanly; the registry stays alive
|
|
33
|
+
* - slow: uplink slower than the configured timeout — the registry gives up
|
|
34
|
+
* instead of hanging
|
|
35
|
+
*
|
|
36
|
+
* Gated on E2E_UPLINK_PORT being set (the harness exports it) so runs against
|
|
37
|
+
* arbitrary registries skip it cleanly, and to the npm adapter so it runs once
|
|
38
|
+
* per suite.
|
|
39
|
+
*/
|
|
40
|
+
function uplinkPort() {
|
|
41
|
+
return parseInt(process.env.E2E_UPLINK_PORT || "", 10);
|
|
42
|
+
}
|
|
43
|
+
async function testUplinkFailure(ctx) {
|
|
44
|
+
const id = ctx.runId;
|
|
45
|
+
const mock = new require_mock_uplink.MockUplink(uplinkPort());
|
|
46
|
+
const cachedPkg = `e2e-uplink-cached-${id}`;
|
|
47
|
+
const cached = mock.addPackage(cachedPkg, 524288);
|
|
48
|
+
await mock.start();
|
|
49
|
+
try {
|
|
50
|
+
await ctx.subTest("healthy uplink: packument and tarball proxied through", async () => {
|
|
51
|
+
const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, cachedPkg);
|
|
52
|
+
require_process.trace("healthy proxy %s → %d | keys=%j", cachedPkg, status, Object.keys(body ?? {}));
|
|
53
|
+
assert.default.strictEqual(status, 200, `Expected 200 proxying ${cachedPkg}, got ${status}`);
|
|
54
|
+
assert.default.strictEqual(body.name, cachedPkg, "Proxied packument name mismatch");
|
|
55
|
+
const tarball = body.versions["1.0.0"].dist.tarball;
|
|
56
|
+
assert.default.ok(tarball.startsWith(ctx.registryUrl), `Expected proxied dist.tarball rewritten to ${ctx.registryUrl}, got ${tarball}`);
|
|
57
|
+
const download = await require_http_client.downloadTarball(tarball);
|
|
58
|
+
assert.default.strictEqual(download.status, 200, "Proxied tarball download failed");
|
|
59
|
+
assert.default.strictEqual(download.sha1, cached.shasum, "Proxied tarball bytes mismatch");
|
|
60
|
+
require_process.trace("mock uplink requests so far: %j", mock.requests);
|
|
61
|
+
assert.default.ok(mock.requests.length > 0, "Expected the registry to hit the mock uplink");
|
|
62
|
+
});
|
|
63
|
+
await ctx.subTest("uplink drops connection mid-tarball", async () => {
|
|
64
|
+
const droppedPkg = `e2e-uplink-dropped-${id}`;
|
|
65
|
+
const dropped = mock.addPackage(droppedPkg, 524288);
|
|
66
|
+
const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, droppedPkg);
|
|
67
|
+
assert.default.strictEqual(status, 200, `Expected 200 for ${droppedPkg} packument`);
|
|
68
|
+
const tarball = body.versions["1.0.0"].dist.tarball;
|
|
69
|
+
mock.mode = "drop-mid-stream";
|
|
70
|
+
let sawCleanFailure = false;
|
|
71
|
+
try {
|
|
72
|
+
const download = await require_http_client.downloadTarball(tarball, { signal: AbortSignal.timeout(15e3) });
|
|
73
|
+
sawCleanFailure = download.status !== 200 || download.sha1 !== dropped.shasum;
|
|
74
|
+
require_process.trace("drop-mid-stream download: status=%d bytes=%d sha1=%s (expected %s)", download.status, download.bytes, download.sha1, dropped.shasum);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
assert.default.ok(err.name !== "TimeoutError", "Registry left the client hanging after the uplink dropped mid-tarball (response never terminated within 15s)");
|
|
77
|
+
sawCleanFailure = true;
|
|
78
|
+
require_process.trace("drop-mid-stream download rejected as expected: %s", err instanceof Error ? err.message : err);
|
|
79
|
+
}
|
|
80
|
+
assert.default.ok(sawCleanFailure, "Expected the client download to fail while the uplink drops mid-stream");
|
|
81
|
+
mock.mode = "ok";
|
|
82
|
+
const retry = await require_http_client.downloadTarball(tarball);
|
|
83
|
+
require_process.trace("retry after recovery → %d bytes=%d sha1=%s", retry.status, retry.bytes, retry.sha1);
|
|
84
|
+
assert.default.strictEqual(retry.status, 200, "Tarball download after uplink recovery failed");
|
|
85
|
+
assert.default.strictEqual(retry.sha1, dropped.shasum, "Registry served a corrupt tarball after a mid-stream uplink failure (poisoned cache?)");
|
|
86
|
+
});
|
|
87
|
+
await ctx.subTest("slow uplink hits the timeout instead of hanging", async () => {
|
|
88
|
+
const slowPkg = `e2e-uplink-slow-${id}`;
|
|
89
|
+
mock.addPackage(slowPkg);
|
|
90
|
+
mock.mode = "slow";
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
const { status } = await require_http_client.fetchPackument(ctx.registryUrl, slowPkg);
|
|
93
|
+
const elapsed = Date.now() - start;
|
|
94
|
+
require_process.trace("slow uplink: %s → %d in %dms", slowPkg, status, elapsed);
|
|
95
|
+
mock.mode = "ok";
|
|
96
|
+
assert.default.ok(status >= 400, `Expected an error status for a timed-out uplink, got ${status}`);
|
|
97
|
+
assert.default.ok(elapsed < 6e3, `Expected the registry to time out (~3s), but the request took ${elapsed}ms`);
|
|
98
|
+
});
|
|
99
|
+
await ctx.subTest("uplink responding 500: cache served, unknown fails cleanly", async () => {
|
|
100
|
+
mock.mode = "error-500";
|
|
101
|
+
const cachedResp = await require_http_client.fetchPackument(ctx.registryUrl, cachedPkg);
|
|
102
|
+
require_process.trace("uplink 500: cached %s → %d", cachedPkg, cachedResp.status);
|
|
103
|
+
assert.default.strictEqual(cachedResp.status, 200, `Expected cached packument for ${cachedPkg} while the uplink returns 500, got ${cachedResp.status}`);
|
|
104
|
+
const unknown = await require_http_client.fetchPackument(ctx.registryUrl, `e2e-uplink-500-missing-${id}`);
|
|
105
|
+
require_process.trace("uplink 500: unknown package → %d | body=%j", unknown.status, unknown.body);
|
|
106
|
+
assert.default.ok(unknown.status >= 400, `Expected a clean 4xx/5xx for an unknown package while the uplink returns 500, got ${unknown.status}`);
|
|
107
|
+
const ping = await fetch(`${ctx.registryUrl}/-/ping`);
|
|
108
|
+
assert.default.strictEqual(ping.status, 200, "Registry stopped responding after uplink 500s");
|
|
109
|
+
mock.mode = "ok";
|
|
110
|
+
});
|
|
111
|
+
} finally {
|
|
112
|
+
await mock.stop();
|
|
113
|
+
}
|
|
114
|
+
await ctx.subTest("uplink down: cached package still served", async () => {
|
|
115
|
+
const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, cachedPkg);
|
|
116
|
+
require_process.trace("uplink down: cached %s → %d", cachedPkg, status);
|
|
117
|
+
assert.default.strictEqual(status, 200, `Expected cached packument for ${cachedPkg} while the uplink is down, got ${status}`);
|
|
118
|
+
const tarball = body.versions["1.0.0"].dist.tarball;
|
|
119
|
+
const download = await require_http_client.downloadTarball(tarball);
|
|
120
|
+
assert.default.strictEqual(download.status, 200, "Cached tarball not served while uplink is down");
|
|
121
|
+
assert.default.strictEqual(download.sha1, cached.shasum, "Cached tarball bytes mismatch");
|
|
122
|
+
});
|
|
123
|
+
await ctx.subTest("uplink down: unknown package fails cleanly", async () => {
|
|
124
|
+
const started = Date.now();
|
|
125
|
+
const { status } = await require_http_client.fetchPackument(ctx.registryUrl, `e2e-uplink-missing-${id}`);
|
|
126
|
+
require_process.trace("uplink down: unknown package → %d in %dms", status, Date.now() - started);
|
|
127
|
+
assert.default.ok(status >= 400, `Expected a clean 4xx/5xx for an unknown package with the uplink down, got ${status}`);
|
|
128
|
+
const ping = await fetch(`${ctx.registryUrl}/-/ping`);
|
|
129
|
+
assert.default.strictEqual(ping.status, 200, "Registry stopped responding after uplink failures");
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
var uplinkFailureScenario = {
|
|
133
|
+
name: "scenario:uplink-failure",
|
|
134
|
+
appliesTo: (adapter) => adapter.type === "npm" && Number.isFinite(uplinkPort()),
|
|
135
|
+
timeout: 12e4,
|
|
136
|
+
run: testUplinkFailure
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
exports.uplinkFailureScenario = uplinkFailureScenario;
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=uplink-failure.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uplink-failure.cjs","names":[],"sources":["../../../src/scenarios/uplink-failure.ts"],"sourcesContent":["import assert from 'assert';\n\nimport { TestContext, TestDefinition } from '../types';\nimport { downloadTarball, fetchPackument } from '../utils/http-client';\nimport { MockUplink } from '../utils/mock-uplink';\nimport { trace } from '../utils/process';\n\n/**\n * Scenario: uplink-failure\n *\n * Verifies how the registry behaves when its uplink misbehaves. Requires the\n * registry to be configured with an uplink pointing at the mock uplink port\n * and a package pattern proxied to it (see `--print-config`):\n *\n * uplinks:\n * e2emock:\n * url: http://localhost:<E2E_UPLINK_PORT>/\n * timeout: 3s\n * max_fails: 100\n * packages:\n * 'e2e-uplink-*':\n * access: $all\n * proxy: e2emock\n *\n * The scenario starts its own HTTP server on that port and flips it between\n * modes:\n * - healthy: packument + tarball proxied through, tarball URL rewritten\n * - drop-mid-stream: uplink kills the socket halfway through a tarball —\n * the client must not receive a \"successful\" corrupt download, and the\n * tarball must be intact on retry (no poisoned cache)\n * - down: previously fetched packages still served from cache; unknown\n * packages fail cleanly; the registry stays alive\n * - slow: uplink slower than the configured timeout — the registry gives up\n * instead of hanging\n *\n * Gated on E2E_UPLINK_PORT being set (the harness exports it) so runs against\n * arbitrary registries skip it cleanly, and to the npm adapter so it runs once\n * per suite.\n */\n\nfunction uplinkPort(): number {\n return parseInt(process.env.E2E_UPLINK_PORT || '', 10);\n}\n\nasync function testUplinkFailure(ctx: TestContext): Promise<void> {\n const id = ctx.runId;\n const mock = new MockUplink(uplinkPort());\n const cachedPkg = `e2e-uplink-cached-${id}`;\n const cached = mock.addPackage(cachedPkg, 512 * 1024);\n\n await mock.start();\n try {\n await ctx.subTest('healthy uplink: packument and tarball proxied through', async () => {\n const { status, body } = await fetchPackument(ctx.registryUrl, cachedPkg);\n trace('healthy proxy %s → %d | keys=%j', cachedPkg, status, Object.keys(body ?? {}));\n assert.strictEqual(status, 200, `Expected 200 proxying ${cachedPkg}, got ${status}`);\n assert.strictEqual(body.name, cachedPkg, 'Proxied packument name mismatch');\n const tarball: string = body.versions['1.0.0'].dist.tarball;\n assert.ok(\n tarball.startsWith(ctx.registryUrl),\n `Expected proxied dist.tarball rewritten to ${ctx.registryUrl}, got ${tarball}`\n );\n const download = await downloadTarball(tarball);\n assert.strictEqual(download.status, 200, 'Proxied tarball download failed');\n assert.strictEqual(download.sha1, cached.shasum, 'Proxied tarball bytes mismatch');\n trace('mock uplink requests so far: %j', mock.requests);\n assert.ok(mock.requests.length > 0, 'Expected the registry to hit the mock uplink');\n });\n\n await ctx.subTest('uplink drops connection mid-tarball', async () => {\n const droppedPkg = `e2e-uplink-dropped-${id}`;\n const dropped = mock.addPackage(droppedPkg, 512 * 1024);\n\n // Packument fetch is unaffected by drop-mid-stream (tarball route only).\n const { status, body } = await fetchPackument(ctx.registryUrl, droppedPkg);\n assert.strictEqual(status, 200, `Expected 200 for ${droppedPkg} packument`);\n const tarball: string = body.versions['1.0.0'].dist.tarball;\n\n mock.mode = 'drop-mid-stream';\n let sawCleanFailure = false;\n try {\n // The registry must terminate the client response promptly when its\n // uplink dies — a client left hanging is a bug, so cap the wait.\n const download = await downloadTarball(tarball, { signal: AbortSignal.timeout(15_000) });\n // A non-200, or a truncated/corrupt body, are both acceptable failures —\n // what must NOT happen is a \"successful\" download with wrong bytes\n // going undetected, so we check integrity ourselves.\n sawCleanFailure = download.status !== 200 || download.sha1 !== dropped.shasum;\n trace(\n 'drop-mid-stream download: status=%d bytes=%d sha1=%s (expected %s)',\n download.status,\n download.bytes,\n download.sha1,\n dropped.shasum\n );\n } catch (err) {\n assert.ok(\n (err as Error).name !== 'TimeoutError',\n 'Registry left the client hanging after the uplink dropped mid-tarball ' +\n '(response never terminated within 15s)'\n );\n sawCleanFailure = true;\n trace(\n 'drop-mid-stream download rejected as expected: %s',\n err instanceof Error ? err.message : err\n );\n }\n assert.ok(\n sawCleanFailure,\n 'Expected the client download to fail while the uplink drops mid-stream'\n );\n\n // Back to healthy: the tarball must be intact (no partial tarball cached).\n mock.mode = 'ok';\n const retry = await downloadTarball(tarball);\n trace('retry after recovery → %d bytes=%d sha1=%s', retry.status, retry.bytes, retry.sha1);\n assert.strictEqual(retry.status, 200, 'Tarball download after uplink recovery failed');\n assert.strictEqual(\n retry.sha1,\n dropped.shasum,\n 'Registry served a corrupt tarball after a mid-stream uplink failure (poisoned cache?)'\n );\n });\n\n await ctx.subTest('slow uplink hits the timeout instead of hanging', async () => {\n const slowPkg = `e2e-uplink-slow-${id}`;\n mock.addPackage(slowPkg);\n mock.mode = 'slow';\n const start = Date.now();\n const { status } = await fetchPackument(ctx.registryUrl, slowPkg);\n const elapsed = Date.now() - start;\n trace('slow uplink: %s → %d in %dms', slowPkg, status, elapsed);\n mock.mode = 'ok';\n assert.ok(status >= 400, `Expected an error status for a timed-out uplink, got ${status}`);\n // Configured uplink timeout is 3s with retry: 0 — anything close to the\n // mock's 10s answer means retries or timeouts are multiplying again\n // (the max_fails-as-retry-limit bug family).\n assert.ok(\n elapsed < 6_000,\n `Expected the registry to time out (~3s), but the request took ${elapsed}ms`\n );\n });\n\n await ctx.subTest('uplink responding 500: cache served, unknown fails cleanly', async () => {\n mock.mode = 'error-500';\n // The already-proxied package must keep working from cache.\n const cachedResp = await fetchPackument(ctx.registryUrl, cachedPkg);\n trace('uplink 500: cached %s → %d', cachedPkg, cachedResp.status);\n assert.strictEqual(\n cachedResp.status,\n 200,\n `Expected cached packument for ${cachedPkg} while the uplink returns 500, got ${cachedResp.status}`\n );\n // An unknown package must fail cleanly, not crash or hang.\n const unknown = await fetchPackument(ctx.registryUrl, `e2e-uplink-500-missing-${id}`);\n trace('uplink 500: unknown package → %d | body=%j', unknown.status, unknown.body);\n assert.ok(\n unknown.status >= 400,\n `Expected a clean 4xx/5xx for an unknown package while the uplink returns 500, got ${unknown.status}`\n );\n const ping = await fetch(`${ctx.registryUrl}/-/ping`);\n assert.strictEqual(ping.status, 200, 'Registry stopped responding after uplink 500s');\n mock.mode = 'ok';\n });\n } finally {\n await mock.stop();\n }\n\n await ctx.subTest('uplink down: cached package still served', async () => {\n const { status, body } = await fetchPackument(ctx.registryUrl, cachedPkg);\n trace('uplink down: cached %s → %d', cachedPkg, status);\n assert.strictEqual(\n status,\n 200,\n `Expected cached packument for ${cachedPkg} while the uplink is down, got ${status}`\n );\n const tarball: string = body.versions['1.0.0'].dist.tarball;\n const download = await downloadTarball(tarball);\n assert.strictEqual(download.status, 200, 'Cached tarball not served while uplink is down');\n assert.strictEqual(download.sha1, cached.shasum, 'Cached tarball bytes mismatch');\n });\n\n await ctx.subTest('uplink down: unknown package fails cleanly', async () => {\n const started = Date.now();\n const { status } = await fetchPackument(ctx.registryUrl, `e2e-uplink-missing-${id}`);\n trace('uplink down: unknown package → %d in %dms', status, Date.now() - started);\n assert.ok(\n status >= 400,\n `Expected a clean 4xx/5xx for an unknown package with the uplink down, got ${status}`\n );\n const ping = await fetch(`${ctx.registryUrl}/-/ping`);\n assert.strictEqual(ping.status, 200, 'Registry stopped responding after uplink failures');\n });\n}\n\nexport const uplinkFailureScenario: TestDefinition = {\n name: 'scenario:uplink-failure',\n // Needs the harness-provided uplink config; skipped cleanly otherwise.\n appliesTo: (adapter) => adapter.type === 'npm' && Number.isFinite(uplinkPort()),\n timeout: 120_000,\n run: testUplinkFailure,\n};\n"],"mappings":"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"}
|