@verdaccio/e2e-cli 3.0.0 → 3.1.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 +136 -0
- package/build/cjs/scenarios/search.cjs.map +1 -0
- package/build/cjs/scenarios/tarballs.cjs +147 -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/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 +134 -0
- package/build/esm/scenarios/search.js.map +1 -0
- package/build/esm/scenarios/tarballs.js +145 -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/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/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,138 @@
|
|
|
1
|
+
import { reportSkipped, reportSubTestResult, reportSuiteStart, reportSummary, reportTestResult, reportTestStart } from "./reporter.js";
|
|
2
|
+
import buildDebug from "debug";
|
|
3
|
+
import { URL } from "url";
|
|
4
|
+
//#region src/runner.ts
|
|
5
|
+
var debug = buildDebug("verdaccio:e2e-cli:runner");
|
|
6
|
+
function getPort(registryUrl) {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(registryUrl);
|
|
9
|
+
return parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80);
|
|
10
|
+
} catch {
|
|
11
|
+
return 4873;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function runSingleTest(adapter, test, registryUrl, token, timeout) {
|
|
15
|
+
const port = getPort(registryUrl);
|
|
16
|
+
let hasSubTests = false;
|
|
17
|
+
const subResults = [];
|
|
18
|
+
const ctx = {
|
|
19
|
+
registryUrl,
|
|
20
|
+
token,
|
|
21
|
+
port,
|
|
22
|
+
exec: adapter.exec.bind(adapter),
|
|
23
|
+
adapter,
|
|
24
|
+
runId: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
25
|
+
async subTest(label, fn) {
|
|
26
|
+
hasSubTests = true;
|
|
27
|
+
const subStart = Date.now();
|
|
28
|
+
try {
|
|
29
|
+
await fn();
|
|
30
|
+
const duration = Date.now() - subStart;
|
|
31
|
+
subResults.push({
|
|
32
|
+
label,
|
|
33
|
+
passed: true,
|
|
34
|
+
duration
|
|
35
|
+
});
|
|
36
|
+
reportSubTestResult(label, true, duration);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const duration = Date.now() - subStart;
|
|
39
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
40
|
+
subResults.push({
|
|
41
|
+
label,
|
|
42
|
+
passed: false,
|
|
43
|
+
duration,
|
|
44
|
+
error
|
|
45
|
+
});
|
|
46
|
+
reportSubTestResult(label, false, duration);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const start = Date.now();
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
resolve({
|
|
55
|
+
name: test.name,
|
|
56
|
+
passed: false,
|
|
57
|
+
duration: Date.now() - start,
|
|
58
|
+
error: `Test timed out after ${timeout}ms`,
|
|
59
|
+
hasSubTests,
|
|
60
|
+
subTests: subResults.length > 0 ? subResults : void 0
|
|
61
|
+
});
|
|
62
|
+
}, timeout);
|
|
63
|
+
test.run(ctx).then(() => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
resolve({
|
|
66
|
+
name: test.name,
|
|
67
|
+
passed: true,
|
|
68
|
+
duration: Date.now() - start,
|
|
69
|
+
hasSubTests,
|
|
70
|
+
subTests: subResults.length > 0 ? subResults : void 0
|
|
71
|
+
});
|
|
72
|
+
}).catch((err) => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
resolve({
|
|
75
|
+
name: test.name,
|
|
76
|
+
passed: false,
|
|
77
|
+
duration: Date.now() - start,
|
|
78
|
+
error: err instanceof Error ? err.message : String(err),
|
|
79
|
+
hasSubTests,
|
|
80
|
+
subTests: subResults.length > 0 ? subResults : void 0
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function runSuite(adapter, tests, registryUrl, token, options) {
|
|
86
|
+
const suiteStart = Date.now();
|
|
87
|
+
const results = [];
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
reportSuiteStart(adapter.name);
|
|
90
|
+
for (const test of tests) {
|
|
91
|
+
if (options.testFilter && options.testFilter.length > 0) {
|
|
92
|
+
if (!options.testFilter.includes(test.name)) continue;
|
|
93
|
+
}
|
|
94
|
+
if (test.requires) {
|
|
95
|
+
if (test.requires.filter((cmd) => !adapter.supports.has(cmd)).length > 0) {
|
|
96
|
+
reportSkipped(test.name);
|
|
97
|
+
skipped++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (test.appliesTo && !test.appliesTo(adapter)) {
|
|
102
|
+
reportSkipped(test.name);
|
|
103
|
+
skipped++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
reportTestStart(adapter.name, test.name);
|
|
107
|
+
const result = await runSingleTest(adapter, test, registryUrl, token, Math.max(options.timeout, test.timeout ?? 0));
|
|
108
|
+
results.push(result);
|
|
109
|
+
reportTestResult(result, result.hasSubTests);
|
|
110
|
+
}
|
|
111
|
+
const passed = results.filter((r) => r.passed).length;
|
|
112
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
113
|
+
return {
|
|
114
|
+
adapter: adapter.name,
|
|
115
|
+
tests: results,
|
|
116
|
+
passed,
|
|
117
|
+
failed,
|
|
118
|
+
skipped,
|
|
119
|
+
duration: Date.now() - suiteStart
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function runAll(adapters, tests, registryUrl, token, options) {
|
|
123
|
+
debug("running %d adapters with %d tests", adapters.length, tests.length);
|
|
124
|
+
const results = [];
|
|
125
|
+
for (const adapter of adapters) {
|
|
126
|
+
const result = await runSuite(adapter, tests, registryUrl, token, options);
|
|
127
|
+
results.push(result);
|
|
128
|
+
}
|
|
129
|
+
reportSummary(results);
|
|
130
|
+
return {
|
|
131
|
+
results,
|
|
132
|
+
exitCode: results.reduce((sum, r) => sum + r.failed, 0) > 0 ? 1 : 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { runAll, runSuite };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","names":[],"sources":["../../src/runner.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport { URL } from 'url';\n\nimport {\n reportSkipped,\n reportSubTestResult,\n reportSuiteStart,\n reportSummary,\n reportTestResult,\n reportTestStart,\n} from './reporter';\nimport {\n PackageManagerAdapter,\n SubTestResult,\n SuiteResult,\n TestContext,\n TestDefinition,\n TestResult,\n} from './types';\n\nconst debug = buildDebug('verdaccio:e2e-cli:runner');\n\nfunction getPort(registryUrl: string): number {\n try {\n const url = new URL(registryUrl);\n return parseInt(url.port, 10) || (url.protocol === 'https:' ? 443 : 80);\n } catch {\n return 4873;\n }\n}\n\nasync function runSingleTest(\n adapter: PackageManagerAdapter,\n test: TestDefinition,\n registryUrl: string,\n token: string,\n timeout: number\n): Promise<TestResult & { hasSubTests: boolean }> {\n const port = getPort(registryUrl);\n let hasSubTests = false;\n const subResults: SubTestResult[] = [];\n const ctx: TestContext = {\n registryUrl,\n token,\n port,\n exec: adapter.exec.bind(adapter),\n adapter,\n runId: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,\n async subTest(label: string, fn: () => Promise<void>): Promise<void> {\n hasSubTests = true;\n const subStart = Date.now();\n try {\n await fn();\n const duration = Date.now() - subStart;\n subResults.push({ label, passed: true, duration });\n reportSubTestResult(label, true, duration);\n } catch (err) {\n const duration = Date.now() - subStart;\n const error = err instanceof Error ? err.message : String(err);\n subResults.push({ label, passed: false, duration, error });\n reportSubTestResult(label, false, duration);\n throw err;\n }\n },\n };\n\n const start = Date.now();\n\n return new Promise<TestResult & { hasSubTests: boolean }>((resolve) => {\n const timer = setTimeout(() => {\n resolve({\n name: test.name,\n passed: false,\n duration: Date.now() - start,\n error: `Test timed out after ${timeout}ms`,\n hasSubTests,\n subTests: subResults.length > 0 ? subResults : undefined,\n });\n }, timeout);\n\n test\n .run(ctx)\n .then(() => {\n clearTimeout(timer);\n resolve({\n name: test.name,\n passed: true,\n duration: Date.now() - start,\n hasSubTests,\n subTests: subResults.length > 0 ? subResults : undefined,\n });\n })\n .catch((err) => {\n clearTimeout(timer);\n resolve({\n name: test.name,\n passed: false,\n duration: Date.now() - start,\n error: err instanceof Error ? err.message : String(err),\n hasSubTests,\n subTests: subResults.length > 0 ? subResults : undefined,\n });\n });\n });\n}\n\nexport async function runSuite(\n adapter: PackageManagerAdapter,\n tests: TestDefinition[],\n registryUrl: string,\n token: string,\n options: { timeout: number; testFilter?: string[] }\n): Promise<SuiteResult> {\n const suiteStart = Date.now();\n const results: TestResult[] = [];\n let skipped = 0;\n\n reportSuiteStart(adapter.name);\n\n for (const test of tests) {\n // Filter by test name if specified\n if (options.testFilter && options.testFilter.length > 0) {\n if (!options.testFilter.includes(test.name)) {\n continue;\n }\n }\n\n // Check if adapter supports required commands\n if (test.requires) {\n const unsupported = test.requires.filter((cmd) => !adapter.supports.has(cmd));\n if (unsupported.length > 0) {\n reportSkipped(test.name);\n skipped++;\n continue;\n }\n }\n\n // Check adapter/version gate (e.g. pnpm-only tests)\n if (test.appliesTo && !test.appliesTo(adapter)) {\n reportSkipped(test.name);\n skipped++;\n continue;\n }\n\n reportTestStart(adapter.name, test.name);\n const timeout = Math.max(options.timeout, test.timeout ?? 0);\n const result = await runSingleTest(adapter, test, registryUrl, token, timeout);\n results.push(result);\n reportTestResult(result, result.hasSubTests);\n }\n\n const passed = results.filter((r) => r.passed).length;\n const failed = results.filter((r) => !r.passed).length;\n\n return {\n adapter: adapter.name,\n tests: results,\n passed,\n failed,\n skipped,\n duration: Date.now() - suiteStart,\n };\n}\n\nexport async function runAll(\n adapters: PackageManagerAdapter[],\n tests: TestDefinition[],\n registryUrl: string,\n token: string,\n options: { timeout: number; concurrency: number; testFilter?: string[] }\n): Promise<{ results: SuiteResult[]; exitCode: number }> {\n debug('running %d adapters with %d tests', adapters.length, tests.length);\n const results: SuiteResult[] = [];\n\n // Run adapters sequentially (each suite is already complex enough)\n for (const adapter of adapters) {\n const result = await runSuite(adapter, tests, registryUrl, token, options);\n results.push(result);\n }\n\n reportSummary(results);\n\n const totalFailed = results.reduce((sum, r) => sum + r.failed, 0);\n return { results, exitCode: totalFailed > 0 ? 1 : 0 };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { installMultipleDepsScenario } from "./install-multiple-deps.js";
|
|
2
|
+
import { metadataScenario } from "./metadata.js";
|
|
3
|
+
import { minimumReleaseAgeScenario } from "./minimum-release-age.js";
|
|
4
|
+
import { searchScenario } from "./search.js";
|
|
5
|
+
import { tarballsScenario } from "./tarballs.js";
|
|
6
|
+
import { uplinkFailureScenario } from "./uplink-failure.js";
|
|
7
|
+
//#region src/scenarios/index.ts
|
|
8
|
+
var allScenarios = [
|
|
9
|
+
installMultipleDepsScenario,
|
|
10
|
+
minimumReleaseAgeScenario,
|
|
11
|
+
tarballsScenario,
|
|
12
|
+
metadataScenario,
|
|
13
|
+
searchScenario,
|
|
14
|
+
uplinkFailureScenario
|
|
15
|
+
];
|
|
16
|
+
//#endregion
|
|
17
|
+
export { allScenarios, installMultipleDepsScenario, metadataScenario, minimumReleaseAgeScenario, searchScenario, tarballsScenario, uplinkFailureScenario };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/scenarios/index.ts"],"sourcesContent":["import { TestDefinition } from '../types';\nimport { installMultipleDepsScenario } from './install-multiple-deps';\nimport { metadataScenario } from './metadata';\nimport { minimumReleaseAgeScenario } from './minimum-release-age';\nimport { searchScenario } from './search';\nimport { tarballsScenario } from './tarballs';\nimport { uplinkFailureScenario } from './uplink-failure';\n\nexport const allScenarios: TestDefinition[] = [\n installMultipleDepsScenario,\n minimumReleaseAgeScenario,\n tarballsScenario,\n metadataScenario,\n searchScenario,\n uplinkFailureScenario,\n];\n\nexport {\n installMultipleDepsScenario,\n minimumReleaseAgeScenario,\n searchScenario,\n tarballsScenario,\n metadataScenario,\n uplinkFailureScenario,\n};\n"],"mappings":";;;;;;;AAQA,IAAa,eAAiC;CAC5C;CACA;CACA;CACA;CACA;CACA;AACF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { trace } from "../utils/process.js";
|
|
2
|
+
import { normalizeInfo } from "../utils/info.js";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import assert from "assert";
|
|
6
|
+
//#region src/scenarios/install-multiple-deps.ts
|
|
7
|
+
/**
|
|
8
|
+
* Scenario: install-multiple-deps
|
|
9
|
+
*
|
|
10
|
+
* Publishes several packages to the registry (some scoped, some unscoped,
|
|
11
|
+
* some with inter-dependencies), then creates a consumer project that
|
|
12
|
+
* depends on all of them and runs `install`.
|
|
13
|
+
*
|
|
14
|
+
* This exercises the registry under realistic load:
|
|
15
|
+
* - many parallel tarball downloads
|
|
16
|
+
* - metadata resolution for multiple packages
|
|
17
|
+
* - scoped-package authentication
|
|
18
|
+
* - transitive dependency resolution
|
|
19
|
+
*/
|
|
20
|
+
var SEED_PACKAGES_COUNT = 5;
|
|
21
|
+
/**
|
|
22
|
+
* Parse `info --json` output across all adapters.
|
|
23
|
+
*/
|
|
24
|
+
function parseInfoOutput(stdout) {
|
|
25
|
+
return normalizeInfo(JSON.parse(stdout));
|
|
26
|
+
}
|
|
27
|
+
async function publishSeedPackage(ctx, pkgName, version, dependencies = {}) {
|
|
28
|
+
trace("publishing seed package %s@%s", pkgName, version);
|
|
29
|
+
const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token, dependencies);
|
|
30
|
+
if (ctx.adapter.type === "yarn-modern") await ctx.adapter.exec({ cwd: tempFolder }, "install");
|
|
31
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
32
|
+
trace("published %s@%s", pkgName, version);
|
|
33
|
+
}
|
|
34
|
+
async function testInstallMultipleDeps(ctx) {
|
|
35
|
+
const id = ctx.runId;
|
|
36
|
+
let projectCwd = "";
|
|
37
|
+
await ctx.subTest("publish seed packages", async () => {
|
|
38
|
+
for (let i = 1; i <= SEED_PACKAGES_COUNT; i++) await publishSeedPackage(ctx, `@verdaccio/seed-${id}-${i}`, "1.0.0");
|
|
39
|
+
const sharedPkg = `@verdaccio/seed-shared-${id}`;
|
|
40
|
+
await publishSeedPackage(ctx, sharedPkg, "1.0.0");
|
|
41
|
+
await publishSeedPackage(ctx, `@verdaccio/seed-mid-${id}`, "1.0.0", {
|
|
42
|
+
[`@verdaccio/seed-${id}-1`]: "1.0.0",
|
|
43
|
+
[`@verdaccio/seed-${id}-2`]: "1.0.0",
|
|
44
|
+
[sharedPkg]: "1.0.0"
|
|
45
|
+
});
|
|
46
|
+
trace("all seed packages published");
|
|
47
|
+
});
|
|
48
|
+
await ctx.subTest("install all dependencies in consumer project", async () => {
|
|
49
|
+
const consumerDeps = {};
|
|
50
|
+
for (let i = 1; i <= SEED_PACKAGES_COUNT; i++) consumerDeps[`@verdaccio/seed-${id}-${i}`] = "1.0.0";
|
|
51
|
+
consumerDeps[`@verdaccio/seed-mid-${id}`] = "1.0.0";
|
|
52
|
+
consumerDeps[`@verdaccio/seed-shared-${id}`] = "1.0.0";
|
|
53
|
+
const consumerName = `@verdaccio/consumer-${id}`;
|
|
54
|
+
const { tempFolder } = await ctx.adapter.prepareProject(consumerName, "1.0.0", ctx.registryUrl, ctx.port, ctx.token, consumerDeps);
|
|
55
|
+
projectCwd = tempFolder;
|
|
56
|
+
trace("installing %d dependencies in consumer project", Object.keys(consumerDeps).length);
|
|
57
|
+
const args = ["install", ...ctx.adapter.registryArg(ctx.registryUrl)];
|
|
58
|
+
await ctx.adapter.exec({ cwd: tempFolder }, ...args);
|
|
59
|
+
trace("install completed for consumer project");
|
|
60
|
+
});
|
|
61
|
+
await ctx.subTest("verify installed packages", async () => {
|
|
62
|
+
for (let i = 1; i <= SEED_PACKAGES_COUNT; i++) {
|
|
63
|
+
const name = `@verdaccio/seed-${id}-${i}`;
|
|
64
|
+
const info = parseInfoOutput((await ctx.adapter.exec({ cwd: projectCwd }, "info", name, "--json", ...ctx.adapter.registryArg(ctx.registryUrl))).stdout);
|
|
65
|
+
assert.strictEqual(info.name, name, `Expected package name "${name}"`);
|
|
66
|
+
assert.strictEqual(info.version, "1.0.0", `Expected version 1.0.0 for ${name}`);
|
|
67
|
+
}
|
|
68
|
+
const midName = `@verdaccio/seed-mid-${id}`;
|
|
69
|
+
const midInfo = parseInfoOutput((await ctx.adapter.exec({ cwd: projectCwd }, "info", midName, "--json", ...ctx.adapter.registryArg(ctx.registryUrl))).stdout);
|
|
70
|
+
assert.strictEqual(midInfo.name, midName);
|
|
71
|
+
const deps = midInfo.dependencies || {};
|
|
72
|
+
assert.strictEqual(deps[`@verdaccio/seed-${id}-1`], "1.0.0", "Intermediate should depend on seed-1");
|
|
73
|
+
assert.strictEqual(deps[`@verdaccio/seed-${id}-2`], "1.0.0", "Intermediate should depend on seed-2");
|
|
74
|
+
assert.strictEqual(deps[`@verdaccio/seed-shared-${id}`], "1.0.0", "Intermediate should depend on shared");
|
|
75
|
+
trace("all packages verified");
|
|
76
|
+
});
|
|
77
|
+
await ctx.subTest("update and re-install", async () => {
|
|
78
|
+
const leafName = `@verdaccio/seed-${id}-1`;
|
|
79
|
+
await publishSeedPackage(ctx, leafName, "2.0.0");
|
|
80
|
+
const sharedName = `@verdaccio/seed-shared-${id}`;
|
|
81
|
+
await publishSeedPackage(ctx, sharedName, "2.0.0");
|
|
82
|
+
const consumerDeps = {
|
|
83
|
+
[leafName]: "^1.0.0",
|
|
84
|
+
[sharedName]: "^1.0.0"
|
|
85
|
+
};
|
|
86
|
+
const consumerName = `@verdaccio/consumer-update-${id}`;
|
|
87
|
+
const { tempFolder } = await ctx.adapter.prepareProject(consumerName, "1.0.0", ctx.registryUrl, ctx.port, ctx.token, consumerDeps);
|
|
88
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "install", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
89
|
+
if ([
|
|
90
|
+
"npm",
|
|
91
|
+
"pnpm",
|
|
92
|
+
"bun"
|
|
93
|
+
].includes(ctx.adapter.type)) for (const name of [leafName, sharedName]) {
|
|
94
|
+
const manifestPath = join(tempFolder, "node_modules", name, "package.json");
|
|
95
|
+
const installed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
96
|
+
trace("on disk: %s@%s (%s)", installed.name, installed.version, manifestPath);
|
|
97
|
+
assert.strictEqual(installed.version, "1.0.0", `Expected ${name}@1.0.0 installed for range ^1.0.0 (latest is 2.0.0), got ${installed.version}`);
|
|
98
|
+
}
|
|
99
|
+
const leafInfo = parseInfoOutput((await ctx.adapter.exec({ cwd: tempFolder }, "info", leafName, "--json", ...ctx.adapter.registryArg(ctx.registryUrl))).stdout);
|
|
100
|
+
trace("registry info for %s: version=%s dist-tags=%j", leafName, leafInfo.version, leafInfo["dist-tags"]);
|
|
101
|
+
assert.strictEqual(leafInfo["dist-tags"]?.latest ?? leafInfo.version, "2.0.0", `Expected latest version of ${leafName} to be 2.0.0`);
|
|
102
|
+
trace("update and re-install verified");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
var installMultipleDepsScenario = {
|
|
106
|
+
name: "scenario:install-multiple-deps",
|
|
107
|
+
requires: [
|
|
108
|
+
"publish",
|
|
109
|
+
"install",
|
|
110
|
+
"info"
|
|
111
|
+
],
|
|
112
|
+
run: testInstallMultipleDeps
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
export { installMultipleDepsScenario };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=install-multiple-deps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-multiple-deps.js","names":[],"sources":["../../../src/scenarios/install-multiple-deps.ts"],"sourcesContent":["import assert from 'assert';\nimport { readFile } from 'fs/promises';\nimport { join } from 'path';\n\nimport { TestContext, TestDefinition } from '../types';\nimport { normalizeInfo } from '../utils/info';\nimport { trace } from '../utils/process';\n\n/**\n * Scenario: install-multiple-deps\n *\n * Publishes several packages to the registry (some scoped, some unscoped,\n * some with inter-dependencies), then creates a consumer project that\n * depends on all of them and runs `install`.\n *\n * This exercises the registry under realistic load:\n * - many parallel tarball downloads\n * - metadata resolution for multiple packages\n * - scoped-package authentication\n * - transitive dependency resolution\n */\n\nconst SEED_PACKAGES_COUNT = 5;\n\n/**\n * Parse `info --json` output across all adapters.\n */\nfunction parseInfoOutput(stdout: string): any {\n return normalizeInfo(JSON.parse(stdout));\n}\n\nasync function publishSeedPackage(\n ctx: TestContext,\n pkgName: string,\n version: string,\n dependencies: Record<string, string> = {}\n): Promise<void> {\n trace('publishing seed package %s@%s', pkgName, version);\n const { tempFolder } = await ctx.adapter.prepareProject(\n pkgName,\n version,\n ctx.registryUrl,\n ctx.port,\n ctx.token,\n dependencies\n );\n\n if (ctx.adapter.type === 'yarn-modern') {\n await ctx.adapter.exec({ cwd: tempFolder }, 'install');\n }\n\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'publish',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n trace('published %s@%s', pkgName, version);\n}\n\nasync function testInstallMultipleDeps(ctx: TestContext): Promise<void> {\n const id = ctx.runId;\n\n // Shared project folder used as cwd for info commands across subtests.\n // Yarn modern requires a project context (package.json + .yarnrc.yml) for `yarn npm info`.\n let projectCwd = '';\n\n // --- Phase 1: Publish a tree of seed packages ---\n await ctx.subTest('publish seed packages', async () => {\n // Leaf packages (no dependencies of their own)\n for (let i = 1; i <= SEED_PACKAGES_COUNT; i++) {\n const name = `@verdaccio/seed-${id}-${i}`;\n await publishSeedPackage(ctx, name, '1.0.0');\n }\n\n // A \"shared\" package that multiple consumers will pull in\n const sharedPkg = `@verdaccio/seed-shared-${id}`;\n await publishSeedPackage(ctx, sharedPkg, '1.0.0');\n\n // An intermediate package that depends on some leaf packages + shared\n const intermediatePkg = `@verdaccio/seed-mid-${id}`;\n await publishSeedPackage(ctx, intermediatePkg, '1.0.0', {\n [`@verdaccio/seed-${id}-1`]: '1.0.0',\n [`@verdaccio/seed-${id}-2`]: '1.0.0',\n [sharedPkg]: '1.0.0',\n });\n\n trace('all seed packages published');\n });\n\n // --- Phase 2: Create a consumer project that depends on everything ---\n await ctx.subTest('install all dependencies in consumer project', async () => {\n const consumerDeps: Record<string, string> = {};\n\n // Direct deps on all leaf packages\n for (let i = 1; i <= SEED_PACKAGES_COUNT; i++) {\n consumerDeps[`@verdaccio/seed-${id}-${i}`] = '1.0.0';\n }\n\n // Also depend on the intermediate (pulls in transitive deps)\n consumerDeps[`@verdaccio/seed-mid-${id}`] = '1.0.0';\n\n // And the shared package directly (already a transitive dep of mid)\n consumerDeps[`@verdaccio/seed-shared-${id}`] = '1.0.0';\n\n const consumerName = `@verdaccio/consumer-${id}`;\n const { tempFolder } = await ctx.adapter.prepareProject(\n consumerName,\n '1.0.0',\n ctx.registryUrl,\n ctx.port,\n ctx.token,\n consumerDeps\n );\n\n projectCwd = tempFolder;\n\n trace('installing %d dependencies in consumer project', Object.keys(consumerDeps).length);\n\n const args = ['install', ...ctx.adapter.registryArg(ctx.registryUrl)];\n await ctx.adapter.exec({ cwd: tempFolder }, ...args);\n\n trace('install completed for consumer project');\n });\n\n // --- Phase 3: Verify all packages resolved correctly ---\n await ctx.subTest('verify installed packages', async () => {\n // Check that each seed package is retrievable via `info`\n for (let i = 1; i <= SEED_PACKAGES_COUNT; i++) {\n const name = `@verdaccio/seed-${id}-${i}`;\n const resp = await ctx.adapter.exec(\n { cwd: projectCwd },\n 'info',\n name,\n '--json',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n const info = parseInfoOutput(resp.stdout);\n assert.strictEqual(info.name, name, `Expected package name \"${name}\"`);\n assert.strictEqual(info.version, '1.0.0', `Expected version 1.0.0 for ${name}`);\n }\n\n // Verify intermediate package has correct dependencies in registry metadata\n const midName = `@verdaccio/seed-mid-${id}`;\n const resp = await ctx.adapter.exec(\n { cwd: projectCwd },\n 'info',\n midName,\n '--json',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n const midInfo = parseInfoOutput(resp.stdout);\n assert.strictEqual(midInfo.name, midName);\n\n const deps = midInfo.dependencies || {};\n assert.strictEqual(\n deps[`@verdaccio/seed-${id}-1`],\n '1.0.0',\n 'Intermediate should depend on seed-1'\n );\n assert.strictEqual(\n deps[`@verdaccio/seed-${id}-2`],\n '1.0.0',\n 'Intermediate should depend on seed-2'\n );\n assert.strictEqual(\n deps[`@verdaccio/seed-shared-${id}`],\n '1.0.0',\n 'Intermediate should depend on shared'\n );\n\n trace('all packages verified');\n });\n\n // --- Phase 4: Publish updated versions and re-install ---\n await ctx.subTest('update and re-install', async () => {\n // Publish v2 of a leaf and the shared package\n const leafName = `@verdaccio/seed-${id}-1`;\n await publishSeedPackage(ctx, leafName, '2.0.0');\n\n const sharedName = `@verdaccio/seed-shared-${id}`;\n await publishSeedPackage(ctx, sharedName, '2.0.0');\n\n // Create a new consumer that uses ^1.0.0 ranges. With 2.0.0 published,\n // the range must still resolve to 1.0.0 (^1.0.0 excludes 2.x) even though\n // dist-tags.latest now points at 2.0.0 — this catches registries that\n // serve range resolution from `latest` instead of the full version list.\n const consumerDeps: Record<string, string> = {\n [leafName]: '^1.0.0',\n [sharedName]: '^1.0.0',\n };\n\n const consumerName = `@verdaccio/consumer-update-${id}`;\n const { tempFolder } = await ctx.adapter.prepareProject(\n consumerName,\n '1.0.0',\n ctx.registryUrl,\n ctx.port,\n ctx.token,\n consumerDeps\n );\n\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'install',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n\n // Verify what actually landed on disk, not just the registry metadata.\n // yarn-modern uses PnP (no node_modules), so the on-disk check applies to\n // the linkers that materialize it.\n if (['npm', 'pnpm', 'bun'].includes(ctx.adapter.type)) {\n for (const name of [leafName, sharedName]) {\n const manifestPath = join(tempFolder, 'node_modules', name, 'package.json');\n const installed = JSON.parse(await readFile(manifestPath, 'utf8'));\n trace('on disk: %s@%s (%s)', installed.name, installed.version, manifestPath);\n assert.strictEqual(\n installed.version,\n '1.0.0',\n `Expected ${name}@1.0.0 installed for range ^1.0.0 (latest is 2.0.0), ` +\n `got ${installed.version}`\n );\n }\n }\n\n // Verify that the registry now serves both versions\n const leafResp = await ctx.adapter.exec(\n { cwd: tempFolder },\n 'info',\n leafName,\n '--json',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n const leafInfo = parseInfoOutput(leafResp.stdout);\n trace(\n 'registry info for %s: version=%s dist-tags=%j',\n leafName,\n leafInfo.version,\n leafInfo['dist-tags']\n );\n\n // dist-tags.latest should be 2.0.0\n assert.strictEqual(\n leafInfo['dist-tags']?.latest ?? leafInfo.version,\n '2.0.0',\n `Expected latest version of ${leafName} to be 2.0.0`\n );\n\n trace('update and re-install verified');\n });\n}\n\nexport const installMultipleDepsScenario: TestDefinition = {\n name: 'scenario:install-multiple-deps',\n requires: ['publish', 'install', 'info'],\n run: testInstallMultipleDeps,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { trace } from "../utils/process.js";
|
|
2
|
+
import { downloadTarball, fetchPackument } from "../utils/http-client.js";
|
|
3
|
+
import assert from "assert";
|
|
4
|
+
//#region src/scenarios/metadata.ts
|
|
5
|
+
/**
|
|
6
|
+
* Scenario: metadata
|
|
7
|
+
*
|
|
8
|
+
* HTTP-level battery for the packument (package metadata) endpoints:
|
|
9
|
+
*
|
|
10
|
+
* - full packument shape: versions, dist-tags, readme, time
|
|
11
|
+
* - abbreviated metadata (Accept: application/vnd.npm.install-v1+json)
|
|
12
|
+
* - ETag + If-None-Match revalidation → 304 (required by Yarn Berry)
|
|
13
|
+
* - dist.tarball URLs rewritten to the registry host and downloadable
|
|
14
|
+
* - scoped packages via the `%2f`-encoded URL form
|
|
15
|
+
* - 404 body shape for unknown packages
|
|
16
|
+
* - coherence after mutations (new version published, version unpublished)
|
|
17
|
+
*
|
|
18
|
+
* Protocol-level and package-manager-agnostic, so it is gated to the npm
|
|
19
|
+
* adapter to run once per suite.
|
|
20
|
+
*/
|
|
21
|
+
async function publishVersion(ctx, pkgName, version) {
|
|
22
|
+
const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token);
|
|
23
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
24
|
+
trace("published %s@%s", pkgName, version);
|
|
25
|
+
return tempFolder;
|
|
26
|
+
}
|
|
27
|
+
async function testMetadata(ctx) {
|
|
28
|
+
const id = ctx.runId;
|
|
29
|
+
const pkg = `e2e-metadata-${id}`;
|
|
30
|
+
let projectFolder = "";
|
|
31
|
+
await ctx.subTest("publish two versions", async () => {
|
|
32
|
+
await publishVersion(ctx, pkg, "1.0.0");
|
|
33
|
+
projectFolder = await publishVersion(ctx, pkg, "1.1.0");
|
|
34
|
+
});
|
|
35
|
+
await ctx.subTest("full packument has versions, dist-tags, readme and time", async () => {
|
|
36
|
+
const { status, body } = await fetchPackument(ctx.registryUrl, pkg);
|
|
37
|
+
trace("full packument → %d | keys=%j dist-tags=%j versions=%j", status, Object.keys(body ?? {}), body?.["dist-tags"], Object.keys(body?.versions ?? {}));
|
|
38
|
+
assert.strictEqual(status, 200, "Expected 200 for full packument");
|
|
39
|
+
assert.strictEqual(body.name, pkg, "Packument name mismatch");
|
|
40
|
+
assert.ok(body.versions["1.0.0"], "Expected version 1.0.0 in packument");
|
|
41
|
+
assert.ok(body.versions["1.1.0"], "Expected version 1.1.0 in packument");
|
|
42
|
+
assert.strictEqual(body["dist-tags"].latest, "1.1.0", "Expected latest dist-tag → 1.1.0");
|
|
43
|
+
assert.ok(body.readme, "Expected readme in full packument");
|
|
44
|
+
assert.ok(body.time?.["1.0.0"], "Expected time entry for 1.0.0");
|
|
45
|
+
assert.ok(body.time?.["1.1.0"], "Expected time entry for 1.1.0");
|
|
46
|
+
});
|
|
47
|
+
await ctx.subTest("abbreviated metadata (install-v1) is served", async () => {
|
|
48
|
+
const { status, body } = await fetchPackument(ctx.registryUrl, pkg, { abbreviated: true });
|
|
49
|
+
trace("abbreviated packument → %d | keys=%j", status, Object.keys(body ?? {}));
|
|
50
|
+
assert.strictEqual(status, 200, "Expected 200 for abbreviated packument");
|
|
51
|
+
assert.ok(body.versions["1.1.0"], "Expected versions in abbreviated metadata");
|
|
52
|
+
assert.ok(body["dist-tags"], "Expected dist-tags in abbreviated metadata");
|
|
53
|
+
assert.strictEqual(body.readme, void 0, "Abbreviated metadata must not include the readme");
|
|
54
|
+
assert.strictEqual(body._id, void 0, "Abbreviated metadata must not include _id");
|
|
55
|
+
assert.strictEqual(body._rev, void 0, "Abbreviated metadata must not include _rev");
|
|
56
|
+
assert.strictEqual(body.readmeFilename, void 0, "Abbreviated metadata must not include readmeFilename");
|
|
57
|
+
assert.ok(body.modified, "Expected \"modified\" field in abbreviated metadata");
|
|
58
|
+
});
|
|
59
|
+
await ctx.subTest("ETag + If-None-Match revalidation returns 304", async () => {
|
|
60
|
+
const etag = (await fetchPackument(ctx.registryUrl, pkg)).headers.get("etag");
|
|
61
|
+
trace("etag on first response: %s", etag);
|
|
62
|
+
assert.ok(etag, "Expected an ETag header on the packument response");
|
|
63
|
+
const revalidated = await fetchPackument(ctx.registryUrl, pkg, { etag });
|
|
64
|
+
trace("revalidation with If-None-Match → %d", revalidated.status);
|
|
65
|
+
assert.strictEqual(revalidated.status, 304, `Expected 304 with If-None-Match: ${etag}, got ${revalidated.status}`);
|
|
66
|
+
});
|
|
67
|
+
await ctx.subTest("ETag changes after a new version is published", async () => {
|
|
68
|
+
const staleEtag = (await fetchPackument(ctx.registryUrl, pkg)).headers.get("etag");
|
|
69
|
+
assert.ok(staleEtag, "Expected an ETag before publishing");
|
|
70
|
+
await publishVersion(ctx, pkg, "1.2.0");
|
|
71
|
+
const after = await fetchPackument(ctx.registryUrl, pkg, { etag: staleEtag });
|
|
72
|
+
trace("stale etag %s → %d | fresh etag %s", staleEtag, after.status, after.headers.get("etag"));
|
|
73
|
+
assert.strictEqual(after.status, 200, `Expected 200 with the stale ETag after publishing 1.2.0, got ${after.status}`);
|
|
74
|
+
assert.ok(after.body?.versions?.["1.2.0"], "Expected 1.2.0 in the revalidated packument");
|
|
75
|
+
const freshEtag = after.headers.get("etag");
|
|
76
|
+
assert.ok(freshEtag, "Expected an ETag on the fresh packument");
|
|
77
|
+
assert.notStrictEqual(freshEtag, staleEtag, "ETag did not change after a publish");
|
|
78
|
+
});
|
|
79
|
+
await ctx.subTest("dist.tarball points at this registry and is downloadable", async () => {
|
|
80
|
+
const { body } = await fetchPackument(ctx.registryUrl, pkg);
|
|
81
|
+
const tarball = body.versions["1.1.0"].dist.tarball;
|
|
82
|
+
assert.ok(tarball.startsWith(ctx.registryUrl), `Expected dist.tarball to be rewritten to ${ctx.registryUrl}, got ${tarball}`);
|
|
83
|
+
const download = await downloadTarball(tarball);
|
|
84
|
+
trace("tarball %s → %d | bytes=%d sha1=%s content-length=%s content-encoding=%s", tarball, download.status, download.bytes, download.sha1, download.headers.get("content-length"), download.headers.get("content-encoding"));
|
|
85
|
+
assert.strictEqual(download.status, 200, `Expected 200 downloading ${tarball}`);
|
|
86
|
+
assert.strictEqual(download.sha1, body.versions["1.1.0"].dist.shasum, "Downloaded tarball does not match advertised shasum");
|
|
87
|
+
});
|
|
88
|
+
await ctx.subTest("scoped packument via %2f-encoded URL", async () => {
|
|
89
|
+
const scopedPkg = `@verdaccio/e2e-metadata-${id}`;
|
|
90
|
+
await publishVersion(ctx, scopedPkg, "1.0.0");
|
|
91
|
+
const { status, body } = await fetchPackument(ctx.registryUrl, scopedPkg);
|
|
92
|
+
assert.strictEqual(status, 200, `Expected 200 for ${scopedPkg}`);
|
|
93
|
+
assert.strictEqual(body.name, scopedPkg, "Scoped packument name mismatch");
|
|
94
|
+
});
|
|
95
|
+
await ctx.subTest("unknown package returns a 404 error body", async () => {
|
|
96
|
+
const { status, body } = await fetchPackument(ctx.registryUrl, `does-not-exist-${id}`);
|
|
97
|
+
trace("unknown package → %d | body=%j", status, body);
|
|
98
|
+
assert.strictEqual(status, 404, `Expected 404, got ${status}`);
|
|
99
|
+
assert.ok(body?.error, "Expected an \"error\" field in the 404 body");
|
|
100
|
+
});
|
|
101
|
+
await ctx.subTest("unpublished version disappears from the packument", async () => {
|
|
102
|
+
await ctx.adapter.exec({ cwd: projectFolder }, "unpublish", `${pkg}@1.0.0`, "--force", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
103
|
+
const { status, body } = await fetchPackument(ctx.registryUrl, pkg);
|
|
104
|
+
trace("after unpublish → %d | versions=%j dist-tags=%j", status, Object.keys(body?.versions ?? {}), body?.["dist-tags"]);
|
|
105
|
+
assert.strictEqual(status, 200, "Expected 200 after unpublishing one version");
|
|
106
|
+
assert.strictEqual(body.versions["1.0.0"], void 0, "Expected 1.0.0 to be gone");
|
|
107
|
+
assert.ok(body.versions["1.1.0"], "Expected 1.1.0 to survive the unpublish");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
var metadataScenario = {
|
|
111
|
+
name: "scenario:metadata",
|
|
112
|
+
requires: ["publish", "unpublish"],
|
|
113
|
+
appliesTo: (adapter) => adapter.type === "npm",
|
|
114
|
+
run: testMetadata
|
|
115
|
+
};
|
|
116
|
+
//#endregion
|
|
117
|
+
export { metadataScenario };
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","names":[],"sources":["../../../src/scenarios/metadata.ts"],"sourcesContent":["import assert from 'assert';\n\nimport { TestContext, TestDefinition } from '../types';\nimport { downloadTarball, fetchPackument } from '../utils/http-client';\nimport { trace } from '../utils/process';\n\n/**\n * Scenario: metadata\n *\n * HTTP-level battery for the packument (package metadata) endpoints:\n *\n * - full packument shape: versions, dist-tags, readme, time\n * - abbreviated metadata (Accept: application/vnd.npm.install-v1+json)\n * - ETag + If-None-Match revalidation → 304 (required by Yarn Berry)\n * - dist.tarball URLs rewritten to the registry host and downloadable\n * - scoped packages via the `%2f`-encoded URL form\n * - 404 body shape for unknown packages\n * - coherence after mutations (new version published, version unpublished)\n *\n * Protocol-level and package-manager-agnostic, so it is gated to the npm\n * adapter to run once per suite.\n */\n\nasync function publishVersion(ctx: TestContext, pkgName: string, version: string): Promise<string> {\n const { tempFolder } = await ctx.adapter.prepareProject(\n pkgName,\n version,\n ctx.registryUrl,\n ctx.port,\n ctx.token\n );\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'publish',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n trace('published %s@%s', pkgName, version);\n return tempFolder;\n}\n\nasync function testMetadata(ctx: TestContext): Promise<void> {\n const id = ctx.runId;\n const pkg = `e2e-metadata-${id}`;\n let projectFolder = '';\n\n await ctx.subTest('publish two versions', async () => {\n await publishVersion(ctx, pkg, '1.0.0');\n projectFolder = await publishVersion(ctx, pkg, '1.1.0');\n });\n\n await ctx.subTest('full packument has versions, dist-tags, readme and time', async () => {\n const { status, body } = await fetchPackument(ctx.registryUrl, pkg);\n trace(\n 'full packument → %d | keys=%j dist-tags=%j versions=%j',\n status,\n Object.keys(body ?? {}),\n body?.['dist-tags'],\n Object.keys(body?.versions ?? {})\n );\n assert.strictEqual(status, 200, 'Expected 200 for full packument');\n assert.strictEqual(body.name, pkg, 'Packument name mismatch');\n assert.ok(body.versions['1.0.0'], 'Expected version 1.0.0 in packument');\n assert.ok(body.versions['1.1.0'], 'Expected version 1.1.0 in packument');\n assert.strictEqual(body['dist-tags'].latest, '1.1.0', 'Expected latest dist-tag → 1.1.0');\n assert.ok(body.readme, 'Expected readme in full packument');\n assert.ok(body.time?.['1.0.0'], 'Expected time entry for 1.0.0');\n assert.ok(body.time?.['1.1.0'], 'Expected time entry for 1.1.0');\n });\n\n await ctx.subTest('abbreviated metadata (install-v1) is served', async () => {\n const { status, body } = await fetchPackument(ctx.registryUrl, pkg, { abbreviated: true });\n trace('abbreviated packument → %d | keys=%j', status, Object.keys(body ?? {}));\n assert.strictEqual(status, 200, 'Expected 200 for abbreviated packument');\n assert.ok(body.versions['1.1.0'], 'Expected versions in abbreviated metadata');\n assert.ok(body['dist-tags'], 'Expected dist-tags in abbreviated metadata');\n // The install-v1 format exists to keep install metadata small: the readme\n // and the internal CouchDB fields must not leak (npm registry contract).\n assert.strictEqual(body.readme, undefined, 'Abbreviated metadata must not include the readme');\n assert.strictEqual(body._id, undefined, 'Abbreviated metadata must not include _id');\n assert.strictEqual(body._rev, undefined, 'Abbreviated metadata must not include _rev');\n assert.strictEqual(\n body.readmeFilename,\n undefined,\n 'Abbreviated metadata must not include readmeFilename'\n );\n // \"modified\" is part of the install-v1 contract\n assert.ok(body.modified, 'Expected \"modified\" field in abbreviated metadata');\n });\n\n await ctx.subTest('ETag + If-None-Match revalidation returns 304', async () => {\n const first = await fetchPackument(ctx.registryUrl, pkg);\n const etag = first.headers.get('etag');\n trace('etag on first response: %s', etag);\n assert.ok(etag, 'Expected an ETag header on the packument response');\n const revalidated = await fetchPackument(ctx.registryUrl, pkg, { etag });\n trace('revalidation with If-None-Match → %d', revalidated.status);\n assert.strictEqual(\n revalidated.status,\n 304,\n `Expected 304 with If-None-Match: ${etag}, got ${revalidated.status}`\n );\n });\n\n await ctx.subTest('ETag changes after a new version is published', async () => {\n const before = await fetchPackument(ctx.registryUrl, pkg);\n const staleEtag = before.headers.get('etag');\n assert.ok(staleEtag, 'Expected an ETag before publishing');\n\n await publishVersion(ctx, pkg, '1.2.0');\n\n // A \"stuck\" ETag would keep serving 304 forever — the stale value must\n // now miss and return the fresh packument with a different ETag.\n const after = await fetchPackument(ctx.registryUrl, pkg, { etag: staleEtag as string });\n trace('stale etag %s → %d | fresh etag %s', staleEtag, after.status, after.headers.get('etag'));\n assert.strictEqual(\n after.status,\n 200,\n `Expected 200 with the stale ETag after publishing 1.2.0, got ${after.status}`\n );\n assert.ok(after.body?.versions?.['1.2.0'], 'Expected 1.2.0 in the revalidated packument');\n const freshEtag = after.headers.get('etag');\n assert.ok(freshEtag, 'Expected an ETag on the fresh packument');\n assert.notStrictEqual(freshEtag, staleEtag, 'ETag did not change after a publish');\n });\n\n await ctx.subTest('dist.tarball points at this registry and is downloadable', async () => {\n const { body } = await fetchPackument(ctx.registryUrl, pkg);\n const tarball: string = body.versions['1.1.0'].dist.tarball;\n assert.ok(\n tarball.startsWith(ctx.registryUrl),\n `Expected dist.tarball to be rewritten to ${ctx.registryUrl}, got ${tarball}`\n );\n const download = await downloadTarball(tarball);\n trace(\n 'tarball %s → %d | bytes=%d sha1=%s content-length=%s content-encoding=%s',\n tarball,\n download.status,\n download.bytes,\n download.sha1,\n download.headers.get('content-length'),\n download.headers.get('content-encoding')\n );\n assert.strictEqual(download.status, 200, `Expected 200 downloading ${tarball}`);\n assert.strictEqual(\n download.sha1,\n body.versions['1.1.0'].dist.shasum,\n 'Downloaded tarball does not match advertised shasum'\n );\n });\n\n await ctx.subTest('scoped packument via %2f-encoded URL', async () => {\n const scopedPkg = `@verdaccio/e2e-metadata-${id}`;\n await publishVersion(ctx, scopedPkg, '1.0.0');\n const { status, body } = await fetchPackument(ctx.registryUrl, scopedPkg);\n assert.strictEqual(status, 200, `Expected 200 for ${scopedPkg}`);\n assert.strictEqual(body.name, scopedPkg, 'Scoped packument name mismatch');\n });\n\n await ctx.subTest('unknown package returns a 404 error body', async () => {\n const { status, body } = await fetchPackument(ctx.registryUrl, `does-not-exist-${id}`);\n trace('unknown package → %d | body=%j', status, body);\n assert.strictEqual(status, 404, `Expected 404, got ${status}`);\n assert.ok(body?.error, 'Expected an \"error\" field in the 404 body');\n });\n\n await ctx.subTest('unpublished version disappears from the packument', async () => {\n // Run from the project folder so its .npmrc (auth token) applies.\n await ctx.adapter.exec(\n { cwd: projectFolder },\n 'unpublish',\n `${pkg}@1.0.0`,\n '--force',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n const { status, body } = await fetchPackument(ctx.registryUrl, pkg);\n trace(\n 'after unpublish → %d | versions=%j dist-tags=%j',\n status,\n Object.keys(body?.versions ?? {}),\n body?.['dist-tags']\n );\n assert.strictEqual(status, 200, 'Expected 200 after unpublishing one version');\n assert.strictEqual(body.versions['1.0.0'], undefined, 'Expected 1.0.0 to be gone');\n assert.ok(body.versions['1.1.0'], 'Expected 1.1.0 to survive the unpublish');\n });\n}\n\nexport const metadataScenario: TestDefinition = {\n name: 'scenario:metadata',\n requires: ['publish', 'unpublish'],\n // Protocol-level battery — run once per suite via the npm adapter.\n appliesTo: (adapter) => adapter.type === 'npm',\n run: testMetadata,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { trace } from "../utils/process.js";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { writeFile } from "fs/promises";
|
|
4
|
+
import assert from "assert";
|
|
5
|
+
//#region src/scenarios/minimum-release-age.ts
|
|
6
|
+
/**
|
|
7
|
+
* Scenario: minimum-release-age (pnpm 11.1+ only)
|
|
8
|
+
*
|
|
9
|
+
* Exercises pnpm's `minimumReleaseAge` cooldown together with
|
|
10
|
+
* `minimumReleaseAgeExclude`. With a 7-day (10080 minute) cooldown, freshly
|
|
11
|
+
* published versions are normally not installable until they age past the
|
|
12
|
+
* threshold — but packages matching the exclude globs bypass the check.
|
|
13
|
+
*
|
|
14
|
+
* minimumReleaseAge: 10080
|
|
15
|
+
* minimumReleaseAgeExclude:
|
|
16
|
+
* - '@verdaccio/*'
|
|
17
|
+
* - 'verdaccio-*'
|
|
18
|
+
*
|
|
19
|
+
* The scenario publishes brand-new (age ~0) packages and asserts:
|
|
20
|
+
* - an excluded scoped package (@verdaccio/*) installs despite the cooldown
|
|
21
|
+
* - an excluded unscoped package (verdaccio-*) installs despite the cooldown
|
|
22
|
+
* - a non-excluded package is blocked by the cooldown (proving it is active)
|
|
23
|
+
*
|
|
24
|
+
* pnpm reads these settings from `pnpm-workspace.yaml`, mirroring the
|
|
25
|
+
* configuration used by the repo root. The cooldown is only enforced from
|
|
26
|
+
* pnpm 11.1.0 onward (11.0.x silently ignores it), so the scenario is gated to
|
|
27
|
+
* that version range — see {@link isPnpm11Plus}.
|
|
28
|
+
*/
|
|
29
|
+
var MINIMUM_RELEASE_AGE = 10080;
|
|
30
|
+
var PNPM_WORKSPACE_YAML = [
|
|
31
|
+
`minimumReleaseAge: ${MINIMUM_RELEASE_AGE}`,
|
|
32
|
+
"minimumReleaseAgeExclude:",
|
|
33
|
+
...["@verdaccio/*", "verdaccio-*"].map((glob) => ` - '${glob}'`),
|
|
34
|
+
""
|
|
35
|
+
].join("\n");
|
|
36
|
+
/**
|
|
37
|
+
* True only for pnpm versions that actually enforce minimumReleaseAge.
|
|
38
|
+
*
|
|
39
|
+
* The cooldown is honored in pnpm 11.1.0+, but it is silently ignored in
|
|
40
|
+
* 11.0.0 / 11.0.1 (and the 11.0.0 release candidates) — those versions install
|
|
41
|
+
* brand-new packages regardless of the configured cooldown. We scope this
|
|
42
|
+
* scenario to pnpm 11.1.0 and newer so it doesn't false-fail on that regression.
|
|
43
|
+
*/
|
|
44
|
+
function isPnpm11Plus(adapter) {
|
|
45
|
+
if (adapter.type !== "pnpm") return false;
|
|
46
|
+
const match = adapter.name.match(/pnpm@(\d+)\.(\d+)/);
|
|
47
|
+
if (!match) return false;
|
|
48
|
+
const major = parseInt(match[1], 10);
|
|
49
|
+
const minor = parseInt(match[2], 10);
|
|
50
|
+
return major > 11 || major === 11 && minor >= 1;
|
|
51
|
+
}
|
|
52
|
+
async function publishPackage(ctx, pkgName, version) {
|
|
53
|
+
trace("publishing %s@%s", pkgName, version);
|
|
54
|
+
const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token);
|
|
55
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
56
|
+
trace("published %s@%s", pkgName, version);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a consumer project with the minimumReleaseAge cooldown configured in
|
|
60
|
+
* pnpm-workspace.yaml, depending on the given packages.
|
|
61
|
+
*/
|
|
62
|
+
async function prepareCooldownConsumer(ctx, consumerName, dependencies) {
|
|
63
|
+
const { tempFolder } = await ctx.adapter.prepareProject(consumerName, "1.0.0", ctx.registryUrl, ctx.port, ctx.token, dependencies);
|
|
64
|
+
await writeFile(join(tempFolder, "pnpm-workspace.yaml"), PNPM_WORKSPACE_YAML);
|
|
65
|
+
trace("prepared cooldown consumer %s at %s", consumerName, tempFolder);
|
|
66
|
+
return tempFolder;
|
|
67
|
+
}
|
|
68
|
+
async function testMinimumReleaseAge(ctx) {
|
|
69
|
+
const id = ctx.runId;
|
|
70
|
+
const excludedScoped = `@verdaccio/release-age-${id}`;
|
|
71
|
+
const excludedUnscoped = `verdaccio-release-age-${id}`;
|
|
72
|
+
const blocked = `e2e-release-age-blocked-${id}`;
|
|
73
|
+
await ctx.subTest("publish fresh packages", async () => {
|
|
74
|
+
await publishPackage(ctx, excludedScoped, "1.0.0");
|
|
75
|
+
await publishPackage(ctx, excludedUnscoped, "1.0.0");
|
|
76
|
+
await publishPackage(ctx, blocked, "1.0.0");
|
|
77
|
+
});
|
|
78
|
+
await ctx.subTest("excluded packages bypass the cooldown", async () => {
|
|
79
|
+
const tempFolder = await prepareCooldownConsumer(ctx, `@verdaccio/consumer-excluded-${id}`, {
|
|
80
|
+
[excludedScoped]: "1.0.0",
|
|
81
|
+
[excludedUnscoped]: "1.0.0"
|
|
82
|
+
});
|
|
83
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "install", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
84
|
+
trace("excluded packages installed successfully under cooldown");
|
|
85
|
+
});
|
|
86
|
+
await ctx.subTest("non-excluded fresh package is blocked", async () => {
|
|
87
|
+
const tempFolder = await prepareCooldownConsumer(ctx, `e2e-consumer-blocked-${id}`, { [blocked]: "1.0.0" });
|
|
88
|
+
let errorMessage = "";
|
|
89
|
+
try {
|
|
90
|
+
await ctx.adapter.exec({ cwd: tempFolder }, "install", "--frozen-lockfile=false", ...ctx.adapter.registryArg(ctx.registryUrl));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
93
|
+
trace("install correctly rejected by cooldown: %s", errorMessage.split("\n")[0]);
|
|
94
|
+
}
|
|
95
|
+
assert.ok(errorMessage, `Expected install of fresh non-excluded package "${blocked}" to be blocked by minimumReleaseAge (${MINIMUM_RELEASE_AGE} min), but it succeeded`);
|
|
96
|
+
assert.ok(/minimumReleaseAge|NO_MATURE_MATCHING_VERSION/i.test(errorMessage), `Install of "${blocked}" failed for an unexpected reason (expected a minimumReleaseAge cooldown error):\n${errorMessage}`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
var minimumReleaseAgeScenario = {
|
|
100
|
+
name: "scenario:minimum-release-age",
|
|
101
|
+
requires: ["publish", "install"],
|
|
102
|
+
appliesTo: isPnpm11Plus,
|
|
103
|
+
run: testMinimumReleaseAge
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
export { isPnpm11Plus, minimumReleaseAgeScenario };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=minimum-release-age.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minimum-release-age.js","names":[],"sources":["../../../src/scenarios/minimum-release-age.ts"],"sourcesContent":["import assert from 'assert';\nimport { writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nimport { PackageManagerAdapter, TestContext, TestDefinition } from '../types';\nimport { trace } from '../utils/process';\n\n/**\n * Scenario: minimum-release-age (pnpm 11.1+ only)\n *\n * Exercises pnpm's `minimumReleaseAge` cooldown together with\n * `minimumReleaseAgeExclude`. With a 7-day (10080 minute) cooldown, freshly\n * published versions are normally not installable until they age past the\n * threshold — but packages matching the exclude globs bypass the check.\n *\n * minimumReleaseAge: 10080\n * minimumReleaseAgeExclude:\n * - '@verdaccio/*'\n * - 'verdaccio-*'\n *\n * The scenario publishes brand-new (age ~0) packages and asserts:\n * - an excluded scoped package (@verdaccio/*) installs despite the cooldown\n * - an excluded unscoped package (verdaccio-*) installs despite the cooldown\n * - a non-excluded package is blocked by the cooldown (proving it is active)\n *\n * pnpm reads these settings from `pnpm-workspace.yaml`, mirroring the\n * configuration used by the repo root. The cooldown is only enforced from\n * pnpm 11.1.0 onward (11.0.x silently ignores it), so the scenario is gated to\n * that version range — see {@link isPnpm11Plus}.\n */\n\nconst MINIMUM_RELEASE_AGE = 10080; // 7 days, in minutes\nconst MINIMUM_RELEASE_AGE_EXCLUDE = ['@verdaccio/*', 'verdaccio-*'];\n\nconst PNPM_WORKSPACE_YAML = [\n `minimumReleaseAge: ${MINIMUM_RELEASE_AGE}`,\n 'minimumReleaseAgeExclude:',\n ...MINIMUM_RELEASE_AGE_EXCLUDE.map((glob) => ` - '${glob}'`),\n '',\n].join('\\n');\n\n/**\n * True only for pnpm versions that actually enforce minimumReleaseAge.\n *\n * The cooldown is honored in pnpm 11.1.0+, but it is silently ignored in\n * 11.0.0 / 11.0.1 (and the 11.0.0 release candidates) — those versions install\n * brand-new packages regardless of the configured cooldown. We scope this\n * scenario to pnpm 11.1.0 and newer so it doesn't false-fail on that regression.\n */\nexport function isPnpm11Plus(adapter: PackageManagerAdapter): boolean {\n if (adapter.type !== 'pnpm') {\n return false;\n }\n const match = adapter.name.match(/pnpm@(\\d+)\\.(\\d+)/);\n if (!match) {\n return false;\n }\n const major = parseInt(match[1], 10);\n const minor = parseInt(match[2], 10);\n return major > 11 || (major === 11 && minor >= 1);\n}\n\nasync function publishPackage(ctx: TestContext, pkgName: string, version: string): Promise<void> {\n trace('publishing %s@%s', pkgName, version);\n const { tempFolder } = await ctx.adapter.prepareProject(\n pkgName,\n version,\n ctx.registryUrl,\n ctx.port,\n ctx.token\n );\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'publish',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n trace('published %s@%s', pkgName, version);\n}\n\n/**\n * Build a consumer project with the minimumReleaseAge cooldown configured in\n * pnpm-workspace.yaml, depending on the given packages.\n */\nasync function prepareCooldownConsumer(\n ctx: TestContext,\n consumerName: string,\n dependencies: Record<string, string>\n): Promise<string> {\n const { tempFolder } = await ctx.adapter.prepareProject(\n consumerName,\n '1.0.0',\n ctx.registryUrl,\n ctx.port,\n ctx.token,\n dependencies\n );\n await writeFile(join(tempFolder, 'pnpm-workspace.yaml'), PNPM_WORKSPACE_YAML);\n trace('prepared cooldown consumer %s at %s', consumerName, tempFolder);\n return tempFolder;\n}\n\nasync function testMinimumReleaseAge(ctx: TestContext): Promise<void> {\n const id = ctx.runId;\n\n const excludedScoped = `@verdaccio/release-age-${id}`;\n const excludedUnscoped = `verdaccio-release-age-${id}`;\n const blocked = `e2e-release-age-blocked-${id}`;\n\n // --- Phase 1: Publish brand-new (age ~0) packages ---\n await ctx.subTest('publish fresh packages', async () => {\n await publishPackage(ctx, excludedScoped, '1.0.0');\n await publishPackage(ctx, excludedUnscoped, '1.0.0');\n await publishPackage(ctx, blocked, '1.0.0');\n });\n\n // --- Phase 2: Excluded packages install despite the cooldown ---\n await ctx.subTest('excluded packages bypass the cooldown', async () => {\n const tempFolder = await prepareCooldownConsumer(ctx, `@verdaccio/consumer-excluded-${id}`, {\n [excludedScoped]: '1.0.0',\n [excludedUnscoped]: '1.0.0',\n });\n\n // Should succeed: both deps match the exclude globs.\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'install',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n\n trace('excluded packages installed successfully under cooldown');\n });\n\n // --- Phase 3: Non-excluded fresh package is blocked by the cooldown ---\n await ctx.subTest('non-excluded fresh package is blocked', async () => {\n const tempFolder = await prepareCooldownConsumer(ctx, `e2e-consumer-blocked-${id}`, {\n [blocked]: '1.0.0',\n });\n\n let errorMessage = '';\n try {\n await ctx.adapter.exec(\n { cwd: tempFolder },\n 'install',\n '--frozen-lockfile=false',\n ...ctx.adapter.registryArg(ctx.registryUrl)\n );\n } catch (err) {\n errorMessage = err instanceof Error ? err.message : String(err);\n trace('install correctly rejected by cooldown: %s', errorMessage.split('\\n')[0]);\n }\n\n assert.ok(\n errorMessage,\n `Expected install of fresh non-excluded package \"${blocked}\" to be blocked by ` +\n `minimumReleaseAge (${MINIMUM_RELEASE_AGE} min), but it succeeded`\n );\n // pnpm reports ERR_PNPM_NO_MATURE_MATCHING_VERSION / \"minimumReleaseAge\" when a\n // version is too new. Assert the failure is the cooldown, not an unrelated error.\n assert.ok(\n /minimumReleaseAge|NO_MATURE_MATCHING_VERSION/i.test(errorMessage),\n `Install of \"${blocked}\" failed for an unexpected reason (expected a minimumReleaseAge ` +\n `cooldown error):\\n${errorMessage}`\n );\n });\n}\n\nexport const minimumReleaseAgeScenario: TestDefinition = {\n name: 'scenario:minimum-release-age',\n requires: ['publish', 'install'],\n appliesTo: isPnpm11Plus,\n run: testMinimumReleaseAge,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;CAC1B,sBAAsB;CACtB;CACA,GAAG,CALgC,gBAAgB,aAKhD,CAAA,CAA4B,KAAK,SAAS,QAAQ,KAAK,EAAE;CAC5D;AACF,CAAC,CAAC,KAAK,IAAI;;;;;;;;;AAUX,SAAgB,aAAa,SAAyC;CACpE,IAAI,QAAQ,SAAS,QACnB,OAAO;CAET,MAAM,QAAQ,QAAQ,KAAK,MAAM,mBAAmB;CACpD,IAAI,CAAC,OACH,OAAO;CAET,MAAM,QAAQ,SAAS,MAAM,IAAI,EAAE;CACnC,MAAM,QAAQ,SAAS,MAAM,IAAI,EAAE;CACnC,OAAO,QAAQ,MAAO,UAAU,MAAM,SAAS;AACjD;AAEA,eAAe,eAAe,KAAkB,SAAiB,SAAgC;CAC/F,MAAM,oBAAoB,SAAS,OAAO;CAC1C,MAAM,EAAE,eAAe,MAAM,IAAI,QAAQ,eACvC,SACA,SACA,IAAI,aACJ,IAAI,MACJ,IAAI,KACN;CACA,MAAM,IAAI,QAAQ,KAChB,EAAE,KAAK,WAAW,GAClB,WACA,GAAG,IAAI,QAAQ,YAAY,IAAI,WAAW,CAC5C;CACA,MAAM,mBAAmB,SAAS,OAAO;AAC3C;;;;;AAMA,eAAe,wBACb,KACA,cACA,cACiB;CACjB,MAAM,EAAE,eAAe,MAAM,IAAI,QAAQ,eACvC,cACA,SACA,IAAI,aACJ,IAAI,MACJ,IAAI,OACJ,YACF;CACA,MAAM,UAAU,KAAK,YAAY,qBAAqB,GAAG,mBAAmB;CAC5E,MAAM,uCAAuC,cAAc,UAAU;CACrE,OAAO;AACT;AAEA,eAAe,sBAAsB,KAAiC;CACpE,MAAM,KAAK,IAAI;CAEf,MAAM,iBAAiB,0BAA0B;CACjD,MAAM,mBAAmB,yBAAyB;CAClD,MAAM,UAAU,2BAA2B;CAG3C,MAAM,IAAI,QAAQ,0BAA0B,YAAY;EACtD,MAAM,eAAe,KAAK,gBAAgB,OAAO;EACjD,MAAM,eAAe,KAAK,kBAAkB,OAAO;EACnD,MAAM,eAAe,KAAK,SAAS,OAAO;CAC5C,CAAC;CAGD,MAAM,IAAI,QAAQ,yCAAyC,YAAY;EACrE,MAAM,aAAa,MAAM,wBAAwB,KAAK,gCAAgC,MAAM;IACzF,iBAAiB;IACjB,mBAAmB;EACtB,CAAC;EAGD,MAAM,IAAI,QAAQ,KAChB,EAAE,KAAK,WAAW,GAClB,WACA,GAAG,IAAI,QAAQ,YAAY,IAAI,WAAW,CAC5C;EAEA,MAAM,yDAAyD;CACjE,CAAC;CAGD,MAAM,IAAI,QAAQ,yCAAyC,YAAY;EACrE,MAAM,aAAa,MAAM,wBAAwB,KAAK,wBAAwB,MAAM,GACjF,UAAU,QACb,CAAC;EAED,IAAI,eAAe;EACnB,IAAI;GACF,MAAM,IAAI,QAAQ,KAChB,EAAE,KAAK,WAAW,GAClB,WACA,2BACA,GAAG,IAAI,QAAQ,YAAY,IAAI,WAAW,CAC5C;EACF,SAAS,KAAK;GACZ,eAAe,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC9D,MAAM,8CAA8C,aAAa,MAAM,IAAI,CAAC,CAAC,EAAE;EACjF;EAEA,OAAO,GACL,cACA,mDAAmD,QAAQ,wCACnC,oBAAoB,wBAC9C;EAGA,OAAO,GACL,gDAAgD,KAAK,YAAY,GACjE,eAAe,QAAQ,oFACA,cACzB;CACF,CAAC;AACH;AAEA,IAAa,4BAA4C;CACvD,MAAM;CACN,UAAU,CAAC,WAAW,SAAS;CAC/B,WAAW;CACX,KAAK;AACP"}
|