@verdaccio/e2e-cli 2.10.4 → 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.
Files changed (122) hide show
  1. package/build/adapters/index.d.ts +0 -1
  2. package/build/cjs/adapters/index.cjs +0 -2
  3. package/build/cjs/adapters/npm.cjs +10 -0
  4. package/build/cjs/adapters/npm.cjs.map +1 -1
  5. package/build/cjs/adapters/pnpm.cjs +11 -1
  6. package/build/cjs/adapters/pnpm.cjs.map +1 -1
  7. package/build/cjs/index.cjs +28 -8
  8. package/build/cjs/index.cjs.map +1 -1
  9. package/build/cjs/runner.cjs +1 -1
  10. package/build/cjs/runner.cjs.map +1 -1
  11. package/build/cjs/scenarios/index.cjs +16 -1
  12. package/build/cjs/scenarios/index.cjs.map +1 -1
  13. package/build/cjs/scenarios/install-multiple-deps.cjs +25 -23
  14. package/build/cjs/scenarios/install-multiple-deps.cjs.map +1 -1
  15. package/build/cjs/scenarios/metadata.cjs +121 -0
  16. package/build/cjs/scenarios/metadata.cjs.map +1 -0
  17. package/build/cjs/scenarios/minimum-release-age.cjs +6 -8
  18. package/build/cjs/scenarios/minimum-release-age.cjs.map +1 -1
  19. package/build/cjs/scenarios/search.cjs +136 -0
  20. package/build/cjs/scenarios/search.cjs.map +1 -0
  21. package/build/cjs/scenarios/tarballs.cjs +147 -0
  22. package/build/cjs/scenarios/tarballs.cjs.map +1 -0
  23. package/build/cjs/scenarios/uplink-failure.cjs +141 -0
  24. package/build/cjs/scenarios/uplink-failure.cjs.map +1 -0
  25. package/build/cjs/tests/audit.cjs +15 -2
  26. package/build/cjs/tests/audit.cjs.map +1 -1
  27. package/build/cjs/tests/ci.cjs +4 -6
  28. package/build/cjs/tests/ci.cjs.map +1 -1
  29. package/build/cjs/tests/index.cjs +7 -3
  30. package/build/cjs/tests/index.cjs.map +1 -1
  31. package/build/cjs/tests/info.cjs +10 -21
  32. package/build/cjs/tests/info.cjs.map +1 -1
  33. package/build/cjs/tests/install.cjs +4 -1
  34. package/build/cjs/tests/install.cjs.map +1 -1
  35. package/build/cjs/tests/ping.cjs +3 -7
  36. package/build/cjs/tests/ping.cjs.map +1 -1
  37. package/build/cjs/tests/publish.cjs +0 -1
  38. package/build/cjs/tests/publish.cjs.map +1 -1
  39. package/build/cjs/tests/search.cjs +9 -5
  40. package/build/cjs/tests/search.cjs.map +1 -1
  41. package/build/cjs/utils/e2e-config.cjs +59 -0
  42. package/build/cjs/utils/e2e-config.cjs.map +1 -0
  43. package/build/cjs/utils/http-client.cjs +111 -0
  44. package/build/cjs/utils/http-client.cjs.map +1 -0
  45. package/build/cjs/utils/mock-uplink.cjs +196 -0
  46. package/build/cjs/utils/mock-uplink.cjs.map +1 -0
  47. package/build/cjs/utils/process.cjs +11 -0
  48. package/build/cjs/utils/process.cjs.map +1 -1
  49. package/build/cjs/utils/project.cjs +1 -1
  50. package/build/cjs/utils/project.cjs.map +1 -1
  51. package/build/cjs/utils/publish.cjs +117 -0
  52. package/build/cjs/utils/publish.cjs.map +1 -0
  53. package/build/esm/adapters/index.js +1 -2
  54. package/build/esm/adapters/npm.js +10 -0
  55. package/build/esm/adapters/npm.js.map +1 -1
  56. package/build/esm/adapters/pnpm.js +11 -1
  57. package/build/esm/adapters/pnpm.js.map +1 -1
  58. package/build/esm/index.js +29 -8
  59. package/build/esm/index.js.map +1 -1
  60. package/build/esm/runner.js +1 -1
  61. package/build/esm/runner.js.map +1 -1
  62. package/build/esm/scenarios/index.js +13 -2
  63. package/build/esm/scenarios/index.js.map +1 -1
  64. package/build/esm/scenarios/install-multiple-deps.js +25 -22
  65. package/build/esm/scenarios/install-multiple-deps.js.map +1 -1
  66. package/build/esm/scenarios/metadata.js +119 -0
  67. package/build/esm/scenarios/metadata.js.map +1 -0
  68. package/build/esm/scenarios/minimum-release-age.js +6 -7
  69. package/build/esm/scenarios/minimum-release-age.js.map +1 -1
  70. package/build/esm/scenarios/search.js +134 -0
  71. package/build/esm/scenarios/search.js.map +1 -0
  72. package/build/esm/scenarios/tarballs.js +145 -0
  73. package/build/esm/scenarios/tarballs.js.map +1 -0
  74. package/build/esm/scenarios/uplink-failure.js +139 -0
  75. package/build/esm/scenarios/uplink-failure.js.map +1 -0
  76. package/build/esm/tests/audit.js +15 -2
  77. package/build/esm/tests/audit.js.map +1 -1
  78. package/build/esm/tests/ci.js +4 -6
  79. package/build/esm/tests/ci.js.map +1 -1
  80. package/build/esm/tests/index.js +4 -0
  81. package/build/esm/tests/index.js.map +1 -1
  82. package/build/esm/tests/info.js +10 -21
  83. package/build/esm/tests/info.js.map +1 -1
  84. package/build/esm/tests/install.js +4 -1
  85. package/build/esm/tests/install.js.map +1 -1
  86. package/build/esm/tests/ping.js +3 -7
  87. package/build/esm/tests/ping.js.map +1 -1
  88. package/build/esm/tests/publish.js +0 -1
  89. package/build/esm/tests/publish.js.map +1 -1
  90. package/build/esm/tests/search.js +9 -5
  91. package/build/esm/tests/search.js.map +1 -1
  92. package/build/esm/utils/e2e-config.js +58 -0
  93. package/build/esm/utils/e2e-config.js.map +1 -0
  94. package/build/esm/utils/http-client.js +105 -0
  95. package/build/esm/utils/http-client.js.map +1 -0
  96. package/build/esm/utils/mock-uplink.js +194 -0
  97. package/build/esm/utils/mock-uplink.js.map +1 -0
  98. package/build/esm/utils/process.js +11 -1
  99. package/build/esm/utils/process.js.map +1 -1
  100. package/build/esm/utils/project.js +1 -1
  101. package/build/esm/utils/project.js.map +1 -1
  102. package/build/esm/utils/publish.js +113 -0
  103. package/build/esm/utils/publish.js.map +1 -0
  104. package/build/index.d.ts +1 -1
  105. package/build/scenarios/index.d.ts +5 -1
  106. package/build/scenarios/metadata.d.ts +2 -0
  107. package/build/scenarios/search.d.ts +2 -0
  108. package/build/scenarios/tarballs.d.ts +2 -0
  109. package/build/scenarios/uplink-failure.d.ts +2 -0
  110. package/build/tests/index.d.ts +2 -2
  111. package/build/types.d.ts +12 -2
  112. package/build/utils/e2e-config.d.ts +21 -0
  113. package/build/utils/http-client.d.ts +49 -0
  114. package/build/utils/mock-uplink.d.ts +47 -0
  115. package/build/utils/process.d.ts +7 -0
  116. package/build/utils/publish.d.ts +17 -0
  117. package/package.json +1 -1
  118. package/build/adapters/yarn-classic.d.ts +0 -2
  119. package/build/cjs/adapters/yarn-classic.cjs +0 -98
  120. package/build/cjs/adapters/yarn-classic.cjs.map +0 -1
  121. package/build/esm/adapters/yarn-classic.js +0 -96
  122. package/build/esm/adapters/yarn-classic.js.map +0 -1
@@ -0,0 +1,121 @@
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
+ let assert = require("assert");
5
+ assert = require_runtime.__toESM(assert, 1);
6
+ //#region src/scenarios/metadata.ts
7
+ /**
8
+ * Scenario: metadata
9
+ *
10
+ * HTTP-level battery for the packument (package metadata) endpoints:
11
+ *
12
+ * - full packument shape: versions, dist-tags, readme, time
13
+ * - abbreviated metadata (Accept: application/vnd.npm.install-v1+json)
14
+ * - ETag + If-None-Match revalidation → 304 (required by Yarn Berry)
15
+ * - dist.tarball URLs rewritten to the registry host and downloadable
16
+ * - scoped packages via the `%2f`-encoded URL form
17
+ * - 404 body shape for unknown packages
18
+ * - coherence after mutations (new version published, version unpublished)
19
+ *
20
+ * Protocol-level and package-manager-agnostic, so it is gated to the npm
21
+ * adapter to run once per suite.
22
+ */
23
+ async function publishVersion(ctx, pkgName, version) {
24
+ const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token);
25
+ await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
26
+ require_process.trace("published %s@%s", pkgName, version);
27
+ return tempFolder;
28
+ }
29
+ async function testMetadata(ctx) {
30
+ const id = ctx.runId;
31
+ const pkg = `e2e-metadata-${id}`;
32
+ let projectFolder = "";
33
+ await ctx.subTest("publish two versions", async () => {
34
+ await publishVersion(ctx, pkg, "1.0.0");
35
+ projectFolder = await publishVersion(ctx, pkg, "1.1.0");
36
+ });
37
+ await ctx.subTest("full packument has versions, dist-tags, readme and time", async () => {
38
+ const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, pkg);
39
+ require_process.trace("full packument → %d | keys=%j dist-tags=%j versions=%j", status, Object.keys(body ?? {}), body?.["dist-tags"], Object.keys(body?.versions ?? {}));
40
+ assert.default.strictEqual(status, 200, "Expected 200 for full packument");
41
+ assert.default.strictEqual(body.name, pkg, "Packument name mismatch");
42
+ assert.default.ok(body.versions["1.0.0"], "Expected version 1.0.0 in packument");
43
+ assert.default.ok(body.versions["1.1.0"], "Expected version 1.1.0 in packument");
44
+ assert.default.strictEqual(body["dist-tags"].latest, "1.1.0", "Expected latest dist-tag → 1.1.0");
45
+ assert.default.ok(body.readme, "Expected readme in full packument");
46
+ assert.default.ok(body.time?.["1.0.0"], "Expected time entry for 1.0.0");
47
+ assert.default.ok(body.time?.["1.1.0"], "Expected time entry for 1.1.0");
48
+ });
49
+ await ctx.subTest("abbreviated metadata (install-v1) is served", async () => {
50
+ const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, pkg, { abbreviated: true });
51
+ require_process.trace("abbreviated packument → %d | keys=%j", status, Object.keys(body ?? {}));
52
+ assert.default.strictEqual(status, 200, "Expected 200 for abbreviated packument");
53
+ assert.default.ok(body.versions["1.1.0"], "Expected versions in abbreviated metadata");
54
+ assert.default.ok(body["dist-tags"], "Expected dist-tags in abbreviated metadata");
55
+ assert.default.strictEqual(body.readme, void 0, "Abbreviated metadata must not include the readme");
56
+ assert.default.strictEqual(body._id, void 0, "Abbreviated metadata must not include _id");
57
+ assert.default.strictEqual(body._rev, void 0, "Abbreviated metadata must not include _rev");
58
+ assert.default.strictEqual(body.readmeFilename, void 0, "Abbreviated metadata must not include readmeFilename");
59
+ assert.default.ok(body.modified, "Expected \"modified\" field in abbreviated metadata");
60
+ });
61
+ await ctx.subTest("ETag + If-None-Match revalidation returns 304", async () => {
62
+ const etag = (await require_http_client.fetchPackument(ctx.registryUrl, pkg)).headers.get("etag");
63
+ require_process.trace("etag on first response: %s", etag);
64
+ assert.default.ok(etag, "Expected an ETag header on the packument response");
65
+ const revalidated = await require_http_client.fetchPackument(ctx.registryUrl, pkg, { etag });
66
+ require_process.trace("revalidation with If-None-Match → %d", revalidated.status);
67
+ assert.default.strictEqual(revalidated.status, 304, `Expected 304 with If-None-Match: ${etag}, got ${revalidated.status}`);
68
+ });
69
+ await ctx.subTest("ETag changes after a new version is published", async () => {
70
+ const staleEtag = (await require_http_client.fetchPackument(ctx.registryUrl, pkg)).headers.get("etag");
71
+ assert.default.ok(staleEtag, "Expected an ETag before publishing");
72
+ await publishVersion(ctx, pkg, "1.2.0");
73
+ const after = await require_http_client.fetchPackument(ctx.registryUrl, pkg, { etag: staleEtag });
74
+ require_process.trace("stale etag %s → %d | fresh etag %s", staleEtag, after.status, after.headers.get("etag"));
75
+ assert.default.strictEqual(after.status, 200, `Expected 200 with the stale ETag after publishing 1.2.0, got ${after.status}`);
76
+ assert.default.ok(after.body?.versions?.["1.2.0"], "Expected 1.2.0 in the revalidated packument");
77
+ const freshEtag = after.headers.get("etag");
78
+ assert.default.ok(freshEtag, "Expected an ETag on the fresh packument");
79
+ assert.default.notStrictEqual(freshEtag, staleEtag, "ETag did not change after a publish");
80
+ });
81
+ await ctx.subTest("dist.tarball points at this registry and is downloadable", async () => {
82
+ const { body } = await require_http_client.fetchPackument(ctx.registryUrl, pkg);
83
+ const tarball = body.versions["1.1.0"].dist.tarball;
84
+ assert.default.ok(tarball.startsWith(ctx.registryUrl), `Expected dist.tarball to be rewritten to ${ctx.registryUrl}, got ${tarball}`);
85
+ const download = await require_http_client.downloadTarball(tarball);
86
+ require_process.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"));
87
+ assert.default.strictEqual(download.status, 200, `Expected 200 downloading ${tarball}`);
88
+ assert.default.strictEqual(download.sha1, body.versions["1.1.0"].dist.shasum, "Downloaded tarball does not match advertised shasum");
89
+ });
90
+ await ctx.subTest("scoped packument via %2f-encoded URL", async () => {
91
+ const scopedPkg = `@verdaccio/e2e-metadata-${id}`;
92
+ await publishVersion(ctx, scopedPkg, "1.0.0");
93
+ const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, scopedPkg);
94
+ assert.default.strictEqual(status, 200, `Expected 200 for ${scopedPkg}`);
95
+ assert.default.strictEqual(body.name, scopedPkg, "Scoped packument name mismatch");
96
+ });
97
+ await ctx.subTest("unknown package returns a 404 error body", async () => {
98
+ const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, `does-not-exist-${id}`);
99
+ require_process.trace("unknown package → %d | body=%j", status, body);
100
+ assert.default.strictEqual(status, 404, `Expected 404, got ${status}`);
101
+ assert.default.ok(body?.error, "Expected an \"error\" field in the 404 body");
102
+ });
103
+ await ctx.subTest("unpublished version disappears from the packument", async () => {
104
+ await ctx.adapter.exec({ cwd: projectFolder }, "unpublish", `${pkg}@1.0.0`, "--force", ...ctx.adapter.registryArg(ctx.registryUrl));
105
+ const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, pkg);
106
+ require_process.trace("after unpublish → %d | versions=%j dist-tags=%j", status, Object.keys(body?.versions ?? {}), body?.["dist-tags"]);
107
+ assert.default.strictEqual(status, 200, "Expected 200 after unpublishing one version");
108
+ assert.default.strictEqual(body.versions["1.0.0"], void 0, "Expected 1.0.0 to be gone");
109
+ assert.default.ok(body.versions["1.1.0"], "Expected 1.1.0 to survive the unpublish");
110
+ });
111
+ }
112
+ var metadataScenario = {
113
+ name: "scenario:metadata",
114
+ requires: ["publish", "unpublish"],
115
+ appliesTo: (adapter) => adapter.type === "npm",
116
+ run: testMetadata
117
+ };
118
+ //#endregion
119
+ exports.metadataScenario = metadataScenario;
120
+
121
+ //# sourceMappingURL=metadata.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.cjs","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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"}
@@ -1,12 +1,10 @@
1
1
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
- let debug = require("debug");
3
- debug = require_runtime.__toESM(debug, 1);
2
+ const require_process = require("../utils/process.cjs");
4
3
  let path = require("path");
5
4
  let fs_promises = require("fs/promises");
6
5
  let assert = require("assert");
7
6
  assert = require_runtime.__toESM(assert, 1);
8
7
  //#region src/scenarios/minimum-release-age.ts
9
- var debug$1 = (0, debug.default)("verdaccio:e2e-cli:scenario:minimum-release-age");
10
8
  /**
11
9
  * Scenario: minimum-release-age (pnpm 11.1+ only)
12
10
  *
@@ -54,10 +52,10 @@ function isPnpm11Plus(adapter) {
54
52
  return major > 11 || major === 11 && minor >= 1;
55
53
  }
56
54
  async function publishPackage(ctx, pkgName, version) {
57
- debug$1("publishing %s@%s", pkgName, version);
55
+ require_process.trace("publishing %s@%s", pkgName, version);
58
56
  const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token);
59
57
  await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
60
- debug$1("published %s@%s", pkgName, version);
58
+ require_process.trace("published %s@%s", pkgName, version);
61
59
  }
62
60
  /**
63
61
  * Build a consumer project with the minimumReleaseAge cooldown configured in
@@ -66,7 +64,7 @@ async function publishPackage(ctx, pkgName, version) {
66
64
  async function prepareCooldownConsumer(ctx, consumerName, dependencies) {
67
65
  const { tempFolder } = await ctx.adapter.prepareProject(consumerName, "1.0.0", ctx.registryUrl, ctx.port, ctx.token, dependencies);
68
66
  await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "pnpm-workspace.yaml"), PNPM_WORKSPACE_YAML);
69
- debug$1("prepared cooldown consumer %s at %s", consumerName, tempFolder);
67
+ require_process.trace("prepared cooldown consumer %s at %s", consumerName, tempFolder);
70
68
  return tempFolder;
71
69
  }
72
70
  async function testMinimumReleaseAge(ctx) {
@@ -85,7 +83,7 @@ async function testMinimumReleaseAge(ctx) {
85
83
  [excludedUnscoped]: "1.0.0"
86
84
  });
87
85
  await ctx.adapter.exec({ cwd: tempFolder }, "install", ...ctx.adapter.registryArg(ctx.registryUrl));
88
- debug$1("excluded packages installed successfully under cooldown");
86
+ require_process.trace("excluded packages installed successfully under cooldown");
89
87
  });
90
88
  await ctx.subTest("non-excluded fresh package is blocked", async () => {
91
89
  const tempFolder = await prepareCooldownConsumer(ctx, `e2e-consumer-blocked-${id}`, { [blocked]: "1.0.0" });
@@ -94,7 +92,7 @@ async function testMinimumReleaseAge(ctx) {
94
92
  await ctx.adapter.exec({ cwd: tempFolder }, "install", "--frozen-lockfile=false", ...ctx.adapter.registryArg(ctx.registryUrl));
95
93
  } catch (err) {
96
94
  errorMessage = err instanceof Error ? err.message : String(err);
97
- debug$1("install correctly rejected by cooldown: %s", errorMessage);
95
+ require_process.trace("install correctly rejected by cooldown: %s", errorMessage.split("\n")[0]);
98
96
  }
99
97
  assert.default.ok(errorMessage, `Expected install of fresh non-excluded package "${blocked}" to be blocked by minimumReleaseAge (${MINIMUM_RELEASE_AGE} min), but it succeeded`);
100
98
  assert.default.ok(/minimumReleaseAge|NO_MATURE_MATCHING_VERSION/i.test(errorMessage), `Install of "${blocked}" failed for an unexpected reason (expected a minimumReleaseAge cooldown error):\n${errorMessage}`);
@@ -1 +1 @@
1
- {"version":3,"file":"minimum-release-age.cjs","names":[],"sources":["../../../src/scenarios/minimum-release-age.ts"],"sourcesContent":["import assert from 'assert';\nimport buildDebug from 'debug';\nimport { writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nimport { PackageManagerAdapter, TestContext, TestDefinition } from '../types';\n\nconst debug = buildDebug('verdaccio:e2e-cli:scenario:minimum-release-age');\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 debug('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 debug('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 debug('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 debug('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 debug('install correctly rejected by cooldown: %s', errorMessage);\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":";;;;;;;;AAOA,IAAM,WAAA,GAAQ,MAAA,QAAA,CAAW,gDAAgD;;;;;;;;;;;;;;;;;;;;;;;;AA0BzE,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,QAAM,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,QAAM,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,OAAA,GAAM,YAAA,UAAA,EAAA,GAAU,KAAA,KAAA,CAAK,YAAY,qBAAqB,GAAG,mBAAmB;CAC5E,QAAM,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,QAAM,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,QAAM,8CAA8C,YAAY;EAClE;EAEA,OAAA,QAAO,GACL,cACA,mDAAmD,QAAQ,wCACnC,oBAAoB,wBAC9C;EAGA,OAAA,QAAO,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"}
1
+ {"version":3,"file":"minimum-release-age.cjs","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,gBAAA,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,gBAAA,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,OAAA,GAAM,YAAA,UAAA,EAAA,GAAU,KAAA,KAAA,CAAK,YAAY,qBAAqB,GAAG,mBAAmB;CAC5E,gBAAA,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,gBAAA,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,gBAAA,MAAM,8CAA8C,aAAa,MAAM,IAAI,CAAC,CAAC,EAAE;EACjF;EAEA,OAAA,QAAO,GACL,cACA,mDAAmD,QAAQ,wCACnC,oBAAoB,wBAC9C;EAGA,OAAA,QAAO,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"}
@@ -0,0 +1,136 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ const require_process = require("../utils/process.cjs");
3
+ require("../utils/mock-uplink.cjs");
4
+ const require_publish = require("../utils/publish.cjs");
5
+ let assert = require("assert");
6
+ assert = require_runtime.__toESM(assert, 1);
7
+ //#region src/scenarios/search.ts
8
+ /**
9
+ * Scenario: search
10
+ *
11
+ * HTTP-level battery for `GET /-/v1/search`, pinning the registry.npmjs.org
12
+ * contract (api-documentation `search.yaml` + npm CLI 12 behavior):
13
+ *
14
+ * - result shape npm CLI depends on: `package.name/version/date` and
15
+ * `maintainers` as an array (format-search-stream maps it without a guard)
16
+ * - `total` = total number of matches, not the size of the returned page
17
+ * - `from`/`size` pagination walks the result set without gaps or overlaps
18
+ * - merged local + uplink results are paginated exactly once (the uplink
19
+ * already applies `from` like npmjs does — re-slicing yields empty pages)
20
+ * - missing `text` → 400 (spec: ERR_TEXT_MISSING), not an empty 200
21
+ * - `time` is an ISO 8601 date-time
22
+ * - oversized `size` is clamped, not an error
23
+ * - a real `npm search --json` on top of the raw HTTP checks
24
+ *
25
+ * The uplink sub-test drives the shared MockUplink (which implements
26
+ * /-/v1/search the way npmjs does) and is gated on E2E_UPLINK_PORT plus the
27
+ * registry being started with the config from --print-config.
28
+ *
29
+ * Protocol-level and package-manager-agnostic, so it is gated to the npm
30
+ * adapter to run once per suite.
31
+ */
32
+ var LOCAL_COUNT = 8;
33
+ async function searchV1(registryUrl, query) {
34
+ const url = new URL("/-/v1/search", registryUrl);
35
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) url.searchParams.set(key, String(value));
36
+ require_process.trace("GET %s", url);
37
+ const started = Date.now();
38
+ const response = await fetch(url);
39
+ let body;
40
+ try {
41
+ body = JSON.parse(await response.text());
42
+ } catch {
43
+ body = void 0;
44
+ }
45
+ 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(", "));
46
+ return {
47
+ status: response.status,
48
+ body
49
+ };
50
+ }
51
+ /**
52
+ * Page through the result set for `text` and assert the pages are disjoint,
53
+ * respect `size`, and together cover exactly `expectedNames`.
54
+ */
55
+ async function assertPaginationCovers(registryUrl, text, size, expectedNames) {
56
+ const seen = /* @__PURE__ */ new Set();
57
+ for (let from = 0; from < expectedNames.length; from += size) {
58
+ const remaining = expectedNames.length - from;
59
+ const { status, body } = await searchV1(registryUrl, {
60
+ text,
61
+ size,
62
+ from
63
+ });
64
+ assert.default.strictEqual(status, 200, `Expected 200 for from=${from}`);
65
+ const names = body.objects.map((o) => o.package.name);
66
+ require_process.trace("page from=%d size=%d → %d results: %s", from, size, names.length, names.join(", "));
67
+ 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"})`);
68
+ for (const name of names) {
69
+ assert.default.ok(!seen.has(name), `Duplicate result "${name}" across pages (from=${from})`);
70
+ seen.add(name);
71
+ }
72
+ }
73
+ const missing = expectedNames.filter((name) => !seen.has(name));
74
+ assert.default.strictEqual(missing.length, 0, `Pagination never returned: ${missing.join(", ")}`);
75
+ }
76
+ async function testSearch(ctx) {
77
+ const id = ctx.runId;
78
+ const localPrefix = `e2e-srchloc-${id}`;
79
+ const localNames = Array.from({ length: LOCAL_COUNT }, (_, i) => `${localPrefix}-p${i + 1}`);
80
+ const failed = [];
81
+ const check = async (label, fn) => {
82
+ try {
83
+ await ctx.subTest(label, fn);
84
+ } catch (err) {
85
+ require_process.trace("check FAILED: %s — %s", label, err instanceof Error ? err.message : err);
86
+ failed.push(`${label} — ${err instanceof Error ? err.message : String(err)}`);
87
+ }
88
+ };
89
+ await ctx.subTest("publish searchable packages", async () => {
90
+ for (const name of localNames) await require_publish.publishLocalPackage(ctx, name, "1.0.0");
91
+ });
92
+ await check("results have the shape npm CLI depends on", async () => {
93
+ const { status, body } = await searchV1(ctx.registryUrl, {
94
+ text: localPrefix,
95
+ size: 250
96
+ });
97
+ assert.default.strictEqual(status, 200, "Expected 200 from /-/v1/search");
98
+ assert.default.ok(Array.isArray(body.objects), "Expected an \"objects\" array");
99
+ require_process.trace("first result object: %j", body.objects[0]);
100
+ assert.default.strictEqual(body.objects.length, LOCAL_COUNT, `Expected ${LOCAL_COUNT} results for ${localPrefix}, got ${body.objects.length}`);
101
+ for (const object of body.objects) {
102
+ assert.default.ok(object.package?.name?.startsWith(localPrefix), "Result outside the query prefix");
103
+ assert.default.ok(Array.isArray(object.package.maintainers), `maintainers must be an array on ${object.package.name}`);
104
+ }
105
+ });
106
+ await check("pagination walks local results without gaps", async () => {
107
+ await assertPaginationCovers(ctx.registryUrl, localPrefix, 3, localNames);
108
+ });
109
+ await check("oversized size is clamped, not an error", async () => {
110
+ const { status, body } = await searchV1(ctx.registryUrl, {
111
+ text: localPrefix,
112
+ size: 1e5
113
+ });
114
+ assert.default.strictEqual(status, 200, "Expected 200 with an oversized size");
115
+ assert.default.ok(body.objects.length <= 250, "Expected the npmjs size cap (250) to apply");
116
+ });
117
+ await check("npm search finds the published packages", async () => {
118
+ const { tempFolder } = await ctx.adapter.prepareProject(`search-battery-client-${id}`, "1.0.0", ctx.registryUrl, ctx.port, ctx.token);
119
+ const resp = await ctx.adapter.exec({ cwd: tempFolder }, "search", localPrefix, "--json", ...ctx.adapter.registryArg(ctx.registryUrl));
120
+ const results = JSON.parse(resp.stdout);
121
+ for (const name of localNames) assert.default.ok(results.find((item) => item.name === name), `Expected ${name} in npm search output`);
122
+ });
123
+ require_process.trace("E2E_UPLINK_PORT not set — skipping merged local+uplink pagination sub-tests");
124
+ if (failed.length > 0) throw new Error(`${failed.length} search contract check(s) failed:\n - ${failed.join("\n - ")}`);
125
+ }
126
+ var searchScenario = {
127
+ name: "scenario:search",
128
+ requires: ["publish", "search"],
129
+ timeout: 9e4,
130
+ appliesTo: (adapter) => adapter.type === "npm",
131
+ run: testSearch
132
+ };
133
+ //#endregion
134
+ exports.searchScenario = searchScenario;
135
+
136
+ //# 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 { 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 * TODO: flip to true once S-1/S-3/S-4/S-5 from the search parity review are\n * fixed in verdaccio (see verdaccio packages/api/src/v1/search.ts). These\n * checks pin the correct npmjs contract but are red against every current\n * registry: real `total` (S-3), 400 without `text` (S-4), ISO `time` (S-5),\n * and single pagination of merged local+uplink results (S-1). Disabled so the\n * scenario gates regressions on the behavior that works today without turning\n * CI red.\n */\nconst PENDING_CONTRACT_CHECKS_ENABLED = false;\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,147 @@
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
+ let path = require("path");
5
+ let fs_promises = require("fs/promises");
6
+ let assert = require("assert");
7
+ assert = require_runtime.__toESM(assert, 1);
8
+ let crypto = require("crypto");
9
+ //#region src/scenarios/tarballs.ts
10
+ /**
11
+ * Scenario: tarballs
12
+ *
13
+ * HTTP-level battery for the tarball endpoints. Publishes a large (~30 MB by
14
+ * default, incompressible) package through the real package manager, then
15
+ * exercises the raw registry protocol with fetch():
16
+ *
17
+ * - full download with Content-Length + shasum/integrity verification
18
+ * - client aborts mid-download (repeatedly) without breaking the registry
19
+ * - concurrent downloads of the same large tarball
20
+ * - 404 behavior for missing packages / versions / mismatched filenames
21
+ * - scoped packages via the npm `%2f`-encoded URL form
22
+ * - end-to-end install of the large package through the package manager
23
+ *
24
+ * The registry must accept large publishes — Verdaccio's default
25
+ * `max_body_size` is 10mb, so the harness config needs e.g. `max_body_size: 100mb`
26
+ * (see `--print-config`). The payload size can be tuned with
27
+ * E2E_LARGE_TARBALL_MB (default 30).
28
+ *
29
+ * Protocol-level and package-manager-agnostic, so it is gated to the npm
30
+ * adapter to run once per suite.
31
+ */
32
+ var LARGE_MB = parseInt(process.env.E2E_LARGE_TARBALL_MB || "30", 10);
33
+ var CONCURRENT_DOWNLOADS = 8;
34
+ async function publishWithPayload(ctx, pkgName, version, payloadBytes) {
35
+ const { tempFolder } = await ctx.adapter.prepareProject(pkgName, version, ctx.registryUrl, ctx.port, ctx.token);
36
+ if (payloadBytes) await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "payload.bin"), (0, crypto.randomBytes)(payloadBytes));
37
+ await ctx.adapter.exec({ cwd: tempFolder }, "publish", ...ctx.adapter.registryArg(ctx.registryUrl));
38
+ require_process.trace("published %s@%s (payload: %d bytes)", pkgName, version, payloadBytes ?? 0);
39
+ }
40
+ async function getDist(ctx, pkgName, version) {
41
+ const { status, body } = await require_http_client.fetchPackument(ctx.registryUrl, pkgName);
42
+ assert.default.strictEqual(status, 200, `Expected 200 fetching packument for ${pkgName}`);
43
+ const dist = body?.versions?.[version]?.dist;
44
+ assert.default.ok(dist?.tarball, `Expected dist.tarball for ${pkgName}@${version}`);
45
+ return dist;
46
+ }
47
+ async function testTarballs(ctx) {
48
+ const id = ctx.runId;
49
+ const largePkg = `e2e-tarball-large-${id}`;
50
+ const largeBytes = LARGE_MB * 1024 * 1024;
51
+ let largeTarballUrl = "";
52
+ let largeSha1 = "";
53
+ await ctx.subTest(`publish large package (~${LARGE_MB} MB)`, async () => {
54
+ await publishWithPayload(ctx, largePkg, "1.0.0", largeBytes);
55
+ });
56
+ await ctx.subTest("download large tarball and verify integrity", async () => {
57
+ const dist = await getDist(ctx, largePkg, "1.0.0");
58
+ largeTarballUrl = dist.tarball;
59
+ const download = await require_http_client.downloadTarball(dist.tarball);
60
+ 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"));
61
+ require_process.trace("advertised dist: shasum=%s integrity=%s", dist.shasum, dist.integrity);
62
+ assert.default.strictEqual(download.status, 200, `Expected 200 downloading ${dist.tarball}`);
63
+ assert.default.ok(download.bytes >= largeBytes, `Expected tarball >= ${largeBytes} bytes (incompressible payload), got ${download.bytes}`);
64
+ const contentLength = download.headers.get("content-length");
65
+ if (contentLength) assert.default.strictEqual(parseInt(contentLength, 10), download.bytes, `Content-Length (${contentLength}) does not match received bytes (${download.bytes})`);
66
+ assert.default.ok(dist.shasum, "Expected dist.shasum on the published version");
67
+ assert.default.strictEqual(download.sha1, dist.shasum, "shasum mismatch on downloaded tarball");
68
+ assert.default.ok(dist.integrity, "Expected dist.integrity on the published version");
69
+ assert.default.strictEqual(download.integrity, dist.integrity, "integrity mismatch on downloaded tarball");
70
+ largeSha1 = download.sha1;
71
+ });
72
+ await ctx.subTest("abort mid-download is handled gracefully", async () => {
73
+ const abortThresholds = [
74
+ 1,
75
+ Math.floor(largeBytes * .25),
76
+ Math.floor(largeBytes * .5)
77
+ ];
78
+ for (const threshold of abortThresholds) {
79
+ const controller = new AbortController();
80
+ let aborted = false;
81
+ try {
82
+ await require_http_client.downloadTarball(largeTarballUrl, {
83
+ signal: controller.signal,
84
+ onChunk: (bytes) => {
85
+ if (bytes >= threshold) controller.abort();
86
+ }
87
+ });
88
+ } catch (err) {
89
+ aborted = true;
90
+ require_process.trace("download aborted at >=%d bytes as expected: %s", threshold, err instanceof Error ? err.message : err);
91
+ }
92
+ assert.default.ok(aborted, `Expected download to abort mid-stream (threshold ${threshold} bytes)`);
93
+ }
94
+ const ping = await fetch(`${ctx.registryUrl}/-/ping`);
95
+ require_process.trace("ping after aborts → %d", ping.status);
96
+ assert.default.strictEqual(ping.status, 200, "Registry did not respond to ping after aborts");
97
+ const download = await require_http_client.downloadTarball(largeTarballUrl);
98
+ require_process.trace("full download after aborts → %d bytes=%d", download.status, download.bytes);
99
+ assert.default.strictEqual(download.status, 200, "Full download after aborts failed");
100
+ assert.default.strictEqual(download.sha1, largeSha1, "Tarball corrupted after aborted downloads");
101
+ });
102
+ await ctx.subTest(`${CONCURRENT_DOWNLOADS} concurrent downloads stay consistent`, async () => {
103
+ const downloads = await Promise.all(Array.from({ length: CONCURRENT_DOWNLOADS }, () => require_http_client.downloadTarball(largeTarballUrl)));
104
+ for (const download of downloads) {
105
+ assert.default.strictEqual(download.status, 200, "Concurrent download failed");
106
+ assert.default.strictEqual(download.sha1, largeSha1, "Concurrent download corrupted");
107
+ }
108
+ });
109
+ await ctx.subTest("missing tarballs return 404", async () => {
110
+ const cases = [
111
+ `${require_http_client.packumentUrl(ctx.registryUrl, largePkg)}/-/${largePkg}-9.9.9.tgz`,
112
+ `${require_http_client.packumentUrl(ctx.registryUrl, `does-not-exist-${id}`)}/-/does-not-exist-${id}-1.0.0.tgz`,
113
+ `${require_http_client.packumentUrl(ctx.registryUrl, largePkg)}/-/some-other-file-1.0.0.tgz`
114
+ ];
115
+ for (const url of cases) {
116
+ const response = await fetch(url);
117
+ const responseBody = await response.text();
118
+ require_process.trace("404 case %s → %d | body=%s", url, response.status, responseBody.slice(0, 200));
119
+ assert.default.strictEqual(response.status, 404, `Expected 404 for ${url}, got ${response.status}`);
120
+ }
121
+ });
122
+ await ctx.subTest("scoped package tarball via %2f-encoded URL", async () => {
123
+ const scopedPkg = `@verdaccio/e2e-tarball-${id}`;
124
+ await publishWithPayload(ctx, scopedPkg, "1.0.0");
125
+ const dist = await getDist(ctx, scopedPkg, "1.0.0");
126
+ const download = await require_http_client.downloadTarball(dist.tarball);
127
+ assert.default.strictEqual(download.status, 200, `Expected 200 downloading ${dist.tarball}`);
128
+ assert.default.ok(download.bytes > 0, "Scoped tarball is empty");
129
+ assert.default.ok(dist.shasum, "Expected dist.shasum on the scoped version");
130
+ assert.default.strictEqual(download.sha1, dist.shasum, "Scoped tarball shasum mismatch");
131
+ });
132
+ await ctx.subTest("package manager installs the large package", async () => {
133
+ const { tempFolder } = await ctx.adapter.prepareProject(`e2e-tarball-consumer-${id}`, "1.0.0", ctx.registryUrl, ctx.port, ctx.token, { [largePkg]: "1.0.0" });
134
+ await ctx.adapter.exec({ cwd: tempFolder }, "install", ...ctx.adapter.registryArg(ctx.registryUrl));
135
+ });
136
+ }
137
+ var tarballsScenario = {
138
+ name: "scenario:tarballs",
139
+ requires: ["publish", "install"],
140
+ appliesTo: (adapter) => adapter.type === "npm",
141
+ timeout: 18e4,
142
+ run: testTarballs
143
+ };
144
+ //#endregion
145
+ exports.tarballsScenario = tarballsScenario;
146
+
147
+ //# 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 { 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 * TODO: flip to true once Verdaccio serves tarballs the way npmjs does.\n * Verified against current registries: local tarballs are always streamed\n * chunked with no Content-Length (even with Accept-Encoding: identity), and\n * the express compression() middleware re-gzips the already-compressed .tgz\n * whenever the client accepts gzip (npm and undici do by default) — wasted\n * CPU per download and the reason Content-Length disappears. These checks pin\n * the correct behavior but are red today.\n */\nconst PENDING_CONTRACT_CHECKS_ENABLED = false;\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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"}