@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.
Files changed (204) hide show
  1. package/bin/e2e-cli.js +6 -26
  2. package/build/adapters/adapter.d.ts +1 -0
  3. package/build/adapters/bun.d.ts +2 -0
  4. package/build/adapters/deno.d.ts +2 -0
  5. package/build/adapters/index.d.ts +5 -0
  6. package/build/adapters/npm.d.ts +2 -0
  7. package/build/adapters/pnpm.d.ts +2 -0
  8. package/build/adapters/yarn-modern.d.ts +2 -0
  9. package/build/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  10. package/build/cjs/adapters/bun.cjs +64 -0
  11. package/build/cjs/adapters/bun.cjs.map +1 -0
  12. package/build/cjs/adapters/deno.cjs +61 -0
  13. package/build/cjs/adapters/deno.cjs.map +1 -0
  14. package/build/cjs/adapters/index.cjs +10 -0
  15. package/build/cjs/adapters/npm.cjs +85 -0
  16. package/build/cjs/adapters/npm.cjs.map +1 -0
  17. package/build/cjs/adapters/pnpm.cjs +114 -0
  18. package/build/cjs/adapters/pnpm.cjs.map +1 -0
  19. package/build/cjs/adapters/yarn-modern.cjs +207 -0
  20. package/build/cjs/adapters/yarn-modern.cjs.map +1 -0
  21. package/build/cjs/index.cjs +186 -11
  22. package/build/cjs/index.cjs.map +1 -0
  23. package/build/cjs/reporter.cjs +113 -0
  24. package/build/cjs/reporter.cjs.map +1 -0
  25. package/build/cjs/runner.cjs +141 -0
  26. package/build/cjs/runner.cjs.map +1 -0
  27. package/build/cjs/scenarios/index.cjs +25 -0
  28. package/build/cjs/scenarios/index.cjs.map +1 -0
  29. package/build/cjs/scenarios/install-multiple-deps.cjs +119 -0
  30. package/build/cjs/scenarios/install-multiple-deps.cjs.map +1 -0
  31. package/build/cjs/scenarios/metadata.cjs +121 -0
  32. package/build/cjs/scenarios/metadata.cjs.map +1 -0
  33. package/build/cjs/scenarios/minimum-release-age.cjs +111 -0
  34. package/build/cjs/scenarios/minimum-release-age.cjs.map +1 -0
  35. package/build/cjs/scenarios/search.cjs +136 -0
  36. package/build/cjs/scenarios/search.cjs.map +1 -0
  37. package/build/cjs/scenarios/tarballs.cjs +147 -0
  38. package/build/cjs/scenarios/tarballs.cjs.map +1 -0
  39. package/build/cjs/scenarios/uplink-failure.cjs +141 -0
  40. package/build/cjs/scenarios/uplink-failure.cjs.map +1 -0
  41. package/build/cjs/tests/audit.cjs +60 -0
  42. package/build/cjs/tests/audit.cjs.map +1 -0
  43. package/build/cjs/tests/ci.cjs +83 -0
  44. package/build/cjs/tests/ci.cjs.map +1 -0
  45. package/build/cjs/tests/deprecate.cjs +108 -0
  46. package/build/cjs/tests/deprecate.cjs.map +1 -0
  47. package/build/cjs/tests/dist-tags.cjs +43 -0
  48. package/build/cjs/tests/dist-tags.cjs.map +1 -0
  49. package/build/cjs/tests/index.cjs +49 -0
  50. package/build/cjs/tests/index.cjs.map +1 -0
  51. package/build/cjs/tests/info.cjs +31 -0
  52. package/build/cjs/tests/info.cjs.map +1 -0
  53. package/build/cjs/tests/install.cjs +30 -0
  54. package/build/cjs/tests/install.cjs.map +1 -0
  55. package/build/cjs/tests/login.cjs +126 -0
  56. package/build/cjs/tests/login.cjs.map +1 -0
  57. package/build/cjs/tests/ping.cjs +26 -0
  58. package/build/cjs/tests/ping.cjs.map +1 -0
  59. package/build/cjs/tests/publish.cjs +36 -0
  60. package/build/cjs/tests/publish.cjs.map +1 -0
  61. package/build/cjs/tests/search.cjs +23 -0
  62. package/build/cjs/tests/search.cjs.map +1 -0
  63. package/build/cjs/tests/unpublish.cjs +40 -0
  64. package/build/cjs/tests/unpublish.cjs.map +1 -0
  65. package/build/cjs/utils/e2e-config.cjs +59 -0
  66. package/build/cjs/utils/e2e-config.cjs.map +1 -0
  67. package/build/cjs/utils/http-client.cjs +111 -0
  68. package/build/cjs/utils/http-client.cjs.map +1 -0
  69. package/build/cjs/utils/info.cjs +25 -0
  70. package/build/cjs/utils/info.cjs.map +1 -0
  71. package/build/cjs/utils/mock-uplink.cjs +196 -0
  72. package/build/cjs/utils/mock-uplink.cjs.map +1 -0
  73. package/build/cjs/utils/process.cjs +96 -0
  74. package/build/cjs/utils/process.cjs.map +1 -0
  75. package/build/cjs/utils/project.cjs +55 -0
  76. package/build/cjs/utils/project.cjs.map +1 -0
  77. package/build/cjs/utils/publish.cjs +117 -0
  78. package/build/cjs/utils/publish.cjs.map +1 -0
  79. package/build/cjs/utils/registry-client.cjs +56 -0
  80. package/build/cjs/utils/registry-client.cjs.map +1 -0
  81. package/build/esm/adapters/bun.js +62 -0
  82. package/build/esm/adapters/bun.js.map +1 -0
  83. package/build/esm/adapters/deno.js +59 -0
  84. package/build/esm/adapters/deno.js.map +1 -0
  85. package/build/esm/adapters/index.js +6 -0
  86. package/build/esm/adapters/npm.js +83 -0
  87. package/build/esm/adapters/npm.js.map +1 -0
  88. package/build/esm/adapters/pnpm.js +112 -0
  89. package/build/esm/adapters/pnpm.js.map +1 -0
  90. package/build/esm/adapters/yarn-modern.js +204 -0
  91. package/build/esm/adapters/yarn-modern.js.map +1 -0
  92. package/build/esm/index.js +174 -10
  93. package/build/esm/index.js.map +1 -0
  94. package/build/esm/reporter.js +108 -0
  95. package/build/esm/reporter.js.map +1 -0
  96. package/build/esm/runner.js +138 -0
  97. package/build/esm/runner.js.map +1 -0
  98. package/build/esm/scenarios/index.js +19 -0
  99. package/build/esm/scenarios/index.js.map +1 -0
  100. package/build/esm/scenarios/install-multiple-deps.js +117 -0
  101. package/build/esm/scenarios/install-multiple-deps.js.map +1 -0
  102. package/build/esm/scenarios/metadata.js +119 -0
  103. package/build/esm/scenarios/metadata.js.map +1 -0
  104. package/build/esm/scenarios/minimum-release-age.js +108 -0
  105. package/build/esm/scenarios/minimum-release-age.js.map +1 -0
  106. package/build/esm/scenarios/search.js +134 -0
  107. package/build/esm/scenarios/search.js.map +1 -0
  108. package/build/esm/scenarios/tarballs.js +145 -0
  109. package/build/esm/scenarios/tarballs.js.map +1 -0
  110. package/build/esm/scenarios/uplink-failure.js +139 -0
  111. package/build/esm/scenarios/uplink-failure.js.map +1 -0
  112. package/build/esm/tests/audit.js +57 -0
  113. package/build/esm/tests/audit.js.map +1 -0
  114. package/build/esm/tests/ci.js +80 -0
  115. package/build/esm/tests/ci.js.map +1 -0
  116. package/build/esm/tests/deprecate.js +105 -0
  117. package/build/esm/tests/deprecate.js.map +1 -0
  118. package/build/esm/tests/dist-tags.js +41 -0
  119. package/build/esm/tests/dist-tags.js.map +1 -0
  120. package/build/esm/tests/index.js +37 -0
  121. package/build/esm/tests/index.js.map +1 -0
  122. package/build/esm/tests/info.js +29 -0
  123. package/build/esm/tests/info.js.map +1 -0
  124. package/build/esm/tests/install.js +28 -0
  125. package/build/esm/tests/install.js.map +1 -0
  126. package/build/esm/tests/login.js +122 -0
  127. package/build/esm/tests/login.js.map +1 -0
  128. package/build/esm/tests/ping.js +24 -0
  129. package/build/esm/tests/ping.js.map +1 -0
  130. package/build/esm/tests/publish.js +34 -0
  131. package/build/esm/tests/publish.js.map +1 -0
  132. package/build/esm/tests/search.js +21 -0
  133. package/build/esm/tests/search.js.map +1 -0
  134. package/build/esm/tests/unpublish.js +38 -0
  135. package/build/esm/tests/unpublish.js.map +1 -0
  136. package/build/esm/utils/e2e-config.js +58 -0
  137. package/build/esm/utils/e2e-config.js.map +1 -0
  138. package/build/esm/utils/http-client.js +105 -0
  139. package/build/esm/utils/http-client.js.map +1 -0
  140. package/build/esm/utils/info.js +24 -0
  141. package/build/esm/utils/info.js.map +1 -0
  142. package/build/esm/utils/mock-uplink.js +194 -0
  143. package/build/esm/utils/mock-uplink.js.map +1 -0
  144. package/build/esm/utils/process.js +92 -0
  145. package/build/esm/utils/process.js.map +1 -0
  146. package/build/esm/utils/project.js +50 -0
  147. package/build/esm/utils/project.js.map +1 -0
  148. package/build/esm/utils/publish.js +113 -0
  149. package/build/esm/utils/publish.js.map +1 -0
  150. package/build/esm/utils/registry-client.js +53 -0
  151. package/build/esm/utils/registry-client.js.map +1 -0
  152. package/build/index.d.ts +6 -15
  153. package/build/reporter.d.ts +7 -0
  154. package/build/runner.d.ts +13 -0
  155. package/build/scenarios/index.d.ts +9 -0
  156. package/build/scenarios/install-multiple-deps.d.ts +2 -0
  157. package/build/scenarios/metadata.d.ts +2 -0
  158. package/build/scenarios/minimum-release-age.d.ts +11 -0
  159. package/build/scenarios/search.d.ts +2 -0
  160. package/build/scenarios/tarballs.d.ts +2 -0
  161. package/build/scenarios/uplink-failure.d.ts +2 -0
  162. package/build/tests/audit.d.ts +2 -0
  163. package/build/tests/ci.d.ts +2 -0
  164. package/build/tests/deprecate.d.ts +2 -0
  165. package/build/tests/dist-tags.d.ts +2 -0
  166. package/build/tests/index.d.ts +15 -0
  167. package/build/tests/info.d.ts +2 -0
  168. package/build/tests/install.d.ts +2 -0
  169. package/build/tests/login.d.ts +2 -0
  170. package/build/tests/ping.d.ts +2 -0
  171. package/build/tests/publish.d.ts +2 -0
  172. package/build/tests/search.d.ts +2 -0
  173. package/build/tests/unpublish.d.ts +2 -0
  174. package/build/types.d.ts +91 -0
  175. package/build/utils/e2e-config.d.ts +21 -0
  176. package/build/utils/http-client.d.ts +49 -0
  177. package/build/utils/index.d.ts +4 -0
  178. package/build/utils/info.d.ts +14 -0
  179. package/build/utils/mock-uplink.d.ts +47 -0
  180. package/build/utils/process.d.ts +11 -0
  181. package/build/utils/project.d.ts +6 -0
  182. package/build/utils/publish.d.ts +17 -0
  183. package/build/utils/registry-client.d.ts +15 -0
  184. package/package.json +16 -14
  185. package/Cargo.lock +0 -1591
  186. package/Cargo.toml +0 -19
  187. package/build/bin/verdaccio-e2e +0 -0
  188. package/scripts/build-js-package.mjs +0 -25
  189. package/src/e2e_tests/audit.rs +0 -51
  190. package/src/e2e_tests/ci.rs +0 -32
  191. package/src/e2e_tests/deprecate.rs +0 -64
  192. package/src/e2e_tests/dist_tags.rs +0 -56
  193. package/src/e2e_tests/info.rs +0 -43
  194. package/src/e2e_tests/install.rs +0 -27
  195. package/src/e2e_tests/login.rs +0 -64
  196. package/src/e2e_tests/mod.rs +0 -104
  197. package/src/e2e_tests/ping.rs +0 -33
  198. package/src/e2e_tests/publish.rs +0 -68
  199. package/src/e2e_tests/search.rs +0 -14
  200. package/src/e2e_tests/unpublish.rs +0 -37
  201. package/src/main.rs +0 -1411
  202. package/src/scenarios/install_multiple_deps.rs +0 -99
  203. package/src/scenarios/minimum_release_age.rs +0 -45
  204. package/src/scenarios/mod.rs +0 -11
package/src/main.rs DELETED
@@ -1,1411 +0,0 @@
1
- use anyhow::{anyhow, bail, Context, Result};
2
- use clap::Parser;
3
- use reqwest::blocking::Client;
4
- use serde_json::{json, Value};
5
- use std::collections::{BTreeMap, BTreeSet};
6
- use std::env;
7
- use std::fs;
8
- use std::io::{Read, Write};
9
- use std::path::{Path, PathBuf};
10
- use std::process::{Command, Stdio};
11
- use std::sync::atomic::{AtomicU64, Ordering};
12
- use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
13
- use tempfile::{Builder, TempDir};
14
- use url::Url;
15
-
16
- mod e2e_tests;
17
- mod scenarios;
18
-
19
- static RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
20
-
21
- const RESET: &str = "\x1b[0m";
22
- const RED: &str = "\x1b[31m";
23
- const GREEN: &str = "\x1b[32m";
24
- const YELLOW: &str = "\x1b[33m";
25
- const CYAN: &str = "\x1b[36m";
26
- const DIM: &str = "\x1b[2m";
27
- const BOLD: &str = "\x1b[1m";
28
-
29
- #[derive(Parser, Debug)]
30
- #[command(
31
- name = "verdaccio-e2e",
32
- disable_version_flag = true,
33
- after_help = "Examples:\n verdaccio-e2e -r http://localhost:4873 --pm npm --pm pnpm\n verdaccio-e2e -r http://localhost:4873 --test publish --test install\n verdaccio-e2e -r http://localhost:4873 --pm yarn-modern@4 -v"
34
- )]
35
- struct Cli {
36
- #[arg(short = 'r', long = "registry")]
37
- registry: String,
38
- #[arg(long = "pm")]
39
- pm: Vec<String>,
40
- #[arg(short = 't', long = "test")]
41
- test: Vec<String>,
42
- #[arg(short = 'c', long = "concurrency", default_value_t = 1)]
43
- concurrency: usize,
44
- #[arg(long = "timeout", default_value_t = 50_000)]
45
- timeout: u64,
46
- #[arg(long = "token")]
47
- token: Option<String>,
48
- #[arg(short = 'v', long = "verbose")]
49
- verbose: bool,
50
- }
51
-
52
- #[derive(Clone, Debug, PartialEq, Eq)]
53
- enum AdapterType {
54
- Npm,
55
- Pnpm,
56
- YarnClassic,
57
- YarnModern,
58
- Bun,
59
- Deno,
60
- }
61
-
62
- #[derive(Clone, Debug)]
63
- struct Adapter {
64
- name: String,
65
- kind: AdapterType,
66
- bin: String,
67
- supports: BTreeSet<&'static str>,
68
- version: String,
69
- yarn_major: u64,
70
- }
71
-
72
- #[derive(Debug)]
73
- struct ExecOutput {
74
- stdout: String,
75
- stderr: String,
76
- }
77
-
78
- #[derive(Debug)]
79
- struct TestResult {
80
- name: &'static str,
81
- passed: bool,
82
- duration: Duration,
83
- error: Option<String>,
84
- sub_tests: Vec<SubTestResult>,
85
- }
86
-
87
- #[derive(Debug)]
88
- struct SubTestResult {
89
- label: &'static str,
90
- passed: bool,
91
- duration: Duration,
92
- error: Option<String>,
93
- }
94
-
95
- #[derive(Debug)]
96
- struct SuiteResult {
97
- adapter: String,
98
- tests: Vec<TestResult>,
99
- passed: usize,
100
- failed: usize,
101
- skipped: usize,
102
- duration: Duration,
103
- }
104
-
105
- struct RunState {
106
- temp_dirs: Vec<TempDir>,
107
- verbose: bool,
108
- }
109
-
110
- struct TestContext<'a> {
111
- registry_url: String,
112
- token: String,
113
- adapter: Adapter,
114
- run_id: String,
115
- timeout: Duration,
116
- deadline: Instant,
117
- state: &'a mut RunState,
118
- sub_results: Vec<SubTestResult>,
119
- }
120
-
121
- struct TestDefinition {
122
- name: &'static str,
123
- requires: &'static [&'static str],
124
- applies_to: fn(&Adapter) -> bool,
125
- run: fn(&mut TestContext<'_>) -> Result<()>,
126
- }
127
-
128
- fn main() {
129
- let code = match run() {
130
- Ok(code) => code,
131
- Err(err) => {
132
- eprintln!("{RED}Error:{RESET} {err:#}");
133
- 1
134
- }
135
- };
136
- std::process::exit(code);
137
- }
138
-
139
- fn run() -> Result<i32> {
140
- let cli = Cli::parse();
141
- let mut state = RunState {
142
- temp_dirs: Vec::new(),
143
- verbose: cli.verbose,
144
- };
145
-
146
- let run_dir = create_temp_dir(&mut state, "e2e-run")?;
147
- env::set_current_dir(run_dir.path()).context("failed to enter isolated run directory")?;
148
-
149
- println!("Checking registry at {}...", cli.registry);
150
- if !ping_registry(&cli.registry) {
151
- bail!("Registry at {} is not reachable", cli.registry);
152
- }
153
- println!("Registry is alive.");
154
-
155
- let token = match cli.token {
156
- Some(token) => token,
157
- None => {
158
- println!("Creating test user...");
159
- let user = format!("e2e-user-{}", now_millis());
160
- let token = create_user(&cli.registry, &user, "e2e-test-password")?;
161
- println!("User \"{user}\" created.");
162
- token
163
- }
164
- };
165
-
166
- let adapters = parse_adapters(&cli.pm, &mut state)?;
167
- println!(
168
- "Adapters: {}",
169
- adapters
170
- .iter()
171
- .map(|adapter| adapter.name.as_str())
172
- .collect::<Vec<_>>()
173
- .join(", ")
174
- );
175
-
176
- let tests = e2e_tests::all_tests();
177
- let selected = if cli.test.is_empty() {
178
- tests.iter().map(|test| test.name).collect::<Vec<_>>()
179
- } else {
180
- cli.test.iter().map(String::as_str).collect::<Vec<_>>()
181
- };
182
- println!("Tests: {}", selected.join(", "));
183
-
184
- let results = run_all(
185
- &adapters,
186
- &tests,
187
- &cli.registry,
188
- &token,
189
- &cli.test,
190
- cli.timeout,
191
- &mut state,
192
- );
193
- report_summary(&results)?;
194
- Ok(if results.iter().any(|suite| suite.failed > 0) {
195
- 1
196
- } else {
197
- 0
198
- })
199
- }
200
-
201
- fn run_all(
202
- adapters: &[Adapter],
203
- tests: &[TestDefinition],
204
- registry_url: &str,
205
- token: &str,
206
- test_filter: &[String],
207
- timeout_ms: u64,
208
- state: &mut RunState,
209
- ) -> Vec<SuiteResult> {
210
- let mut results = Vec::new();
211
- for adapter in adapters {
212
- results.push(run_suite(
213
- adapter,
214
- tests,
215
- registry_url,
216
- token,
217
- test_filter,
218
- timeout_ms,
219
- state,
220
- ));
221
- }
222
- results
223
- }
224
-
225
- fn run_suite(
226
- adapter: &Adapter,
227
- tests: &[TestDefinition],
228
- registry_url: &str,
229
- token: &str,
230
- test_filter: &[String],
231
- timeout_ms: u64,
232
- state: &mut RunState,
233
- ) -> SuiteResult {
234
- let suite_start = Instant::now();
235
- let mut results = Vec::new();
236
- let mut skipped = 0usize;
237
- println!("\n{BOLD}{CYAN}{}{RESET}", adapter.name);
238
-
239
- for test in tests {
240
- if !test_filter.is_empty() && !test_filter.iter().any(|name| name == test.name) {
241
- continue;
242
- }
243
- let unsupported = test
244
- .requires
245
- .iter()
246
- .any(|command| !adapter.supports.contains(command));
247
- if unsupported || !(test.applies_to)(adapter) {
248
- println!(" {YELLOW}SKIP{RESET} {}", test.name);
249
- skipped += 1;
250
- continue;
251
- }
252
-
253
- println!(" {DIM}running{RESET} {} > {}...", adapter.name, test.name);
254
- let started = Instant::now();
255
- let timeout = Duration::from_millis(timeout_ms);
256
- let deadline = started + timeout;
257
- let mut ctx = TestContext {
258
- registry_url: registry_url.to_string(),
259
- token: token.to_string(),
260
- adapter: adapter.clone(),
261
- run_id: run_id(),
262
- timeout,
263
- deadline,
264
- state,
265
- sub_results: Vec::new(),
266
- };
267
-
268
- let result = match (test.run)(&mut ctx) {
269
- Ok(()) if started.elapsed() > timeout => TestResult {
270
- name: test.name,
271
- passed: false,
272
- duration: started.elapsed(),
273
- error: Some(format!("Test timed out after {timeout_ms}ms")),
274
- sub_tests: ctx.sub_results,
275
- },
276
- Ok(()) => TestResult {
277
- name: test.name,
278
- passed: true,
279
- duration: started.elapsed(),
280
- error: None,
281
- sub_tests: ctx.sub_results,
282
- },
283
- Err(err) => TestResult {
284
- name: test.name,
285
- passed: false,
286
- duration: started.elapsed(),
287
- error: Some(format!("{err:#}")),
288
- sub_tests: ctx.sub_results,
289
- },
290
- };
291
- report_test_result(&result);
292
- results.push(result);
293
- }
294
-
295
- let passed = results.iter().filter(|result| result.passed).count();
296
- let failed = results.iter().filter(|result| !result.passed).count();
297
- SuiteResult {
298
- adapter: adapter.name.clone(),
299
- tests: results,
300
- passed,
301
- failed,
302
- skipped,
303
- duration: suite_start.elapsed(),
304
- }
305
- }
306
-
307
- fn parse_adapters(filters: &[String], state: &mut RunState) -> Result<Vec<Adapter>> {
308
- if filters.is_empty() {
309
- return Ok(vec![create_adapter("npm", None, None, state)?]);
310
- }
311
- filters
312
- .iter()
313
- .map(|filter| {
314
- let (name, version, bin_path) = parse_pm_spec(filter);
315
- create_adapter(&name, version.as_deref(), bin_path.as_deref(), state)
316
- })
317
- .collect()
318
- }
319
-
320
- fn parse_pm_spec(filter: &str) -> (String, Option<String>, Option<String>) {
321
- if let Some((name, bin)) = filter.split_once('=') {
322
- return (name.to_ascii_lowercase(), None, Some(bin.to_string()));
323
- }
324
- if let Some((name, version)) = filter.split_once('@') {
325
- return (name.to_ascii_lowercase(), Some(version.to_string()), None);
326
- }
327
- (filter.to_ascii_lowercase(), None, None)
328
- }
329
-
330
- fn create_adapter(
331
- name: &str,
332
- version: Option<&str>,
333
- bin_path: Option<&str>,
334
- state: &mut RunState,
335
- ) -> Result<Adapter> {
336
- match name {
337
- "npm" => {
338
- let bin = resolve_installed_bin(bin_path, version, "npm", "npm", "npm", state)?;
339
- let resolved = detect_version(&bin, &[])?;
340
- Ok(Adapter {
341
- name: format!("npm@{resolved}"),
342
- kind: AdapterType::Npm,
343
- bin,
344
- supports: set(&["publish", "unpublish", "install", "info", "audit", "deprecate", "dist-tag", "ping", "search"]),
345
- version: resolved,
346
- yarn_major: 0,
347
- })
348
- }
349
- "pnpm" => {
350
- let bin = resolve_installed_bin(bin_path, version, "pnpm", "pnpm", "pnpm", state)?;
351
- let resolved = detect_version(&bin, &[])?;
352
- let major = parse_major(&resolved);
353
- let supports = if major >= 11 {
354
- set(&["publish", "unpublish", "install", "info", "audit", "deprecate"])
355
- } else {
356
- set(&["publish", "unpublish", "install", "info", "audit", "deprecate", "dist-tag", "ping", "search"])
357
- };
358
- Ok(Adapter {
359
- name: format!("pnpm@{resolved}"),
360
- kind: AdapterType::Pnpm,
361
- bin,
362
- supports,
363
- version: resolved,
364
- yarn_major: 0,
365
- })
366
- }
367
- "yarn-classic" | "yarn1" => {
368
- let bin = match (bin_path, version) {
369
- (Some(bin), _) => bin.to_string(),
370
- (_, Some(version)) => {
371
- let package = if version.starts_with('1') {
372
- format!("yarn@{version}")
373
- } else {
374
- "yarn@1".to_string()
375
- };
376
- install_package_bin(&package, "node_modules/.bin/yarn", state)?
377
- }
378
- _ => resolve_yarn_classic_bin(state)?,
379
- };
380
- let resolved = detect_version_with_env(&bin, &[], yarn_base_env())?;
381
- Ok(Adapter {
382
- name: format!("yarn-classic@{resolved}"),
383
- kind: AdapterType::YarnClassic,
384
- bin,
385
- supports: set(&["publish", "install", "info", "audit"]),
386
- version: resolved,
387
- yarn_major: 1,
388
- })
389
- }
390
- "yarn-modern" | "yarn" => {
391
- let bin = match (bin_path, version) {
392
- (Some(bin), _) => bin.to_string(),
393
- (_, Some(version)) => install_package_bin(
394
- &format!("@yarnpkg/cli-dist@{version}"),
395
- "node_modules/@yarnpkg/cli-dist/bin/yarn.js",
396
- state,
397
- )?,
398
- _ => resolve_yarn_modern_bin(state)?,
399
- };
400
- let resolved = detect_version_with_env(&bin, &[], yarn_base_env())?;
401
- let major = parse_major(&resolved);
402
- Ok(Adapter {
403
- name: format!("yarn-modern@{resolved}"),
404
- kind: AdapterType::YarnModern,
405
- bin,
406
- supports: set(&["publish", "install", "info", "ping", "deprecate", "login"]),
407
- version: resolved,
408
- yarn_major: major,
409
- })
410
- }
411
- "bun" => {
412
- let bin = bin_path.unwrap_or("bun").to_string();
413
- let resolved = detect_version(&bin, &[])?;
414
- Ok(Adapter {
415
- name: format!("bun@{resolved}"),
416
- kind: AdapterType::Bun,
417
- bin,
418
- supports: set(&["publish", "install", "info", "audit"]),
419
- version: resolved,
420
- yarn_major: 0,
421
- })
422
- }
423
- "deno" => {
424
- let bin = bin_path.unwrap_or("deno").to_string();
425
- let output = command_output(&bin, &["--version"], None, &[], false, None)?;
426
- let resolved = output
427
- .stdout
428
- .lines()
429
- .find_map(|line| line.strip_prefix("deno ").map(str::to_string))
430
- .unwrap_or_else(|| "unknown".to_string());
431
- Ok(Adapter {
432
- name: format!("deno@{resolved}"),
433
- kind: AdapterType::Deno,
434
- bin,
435
- supports: set(&["install", "info"]),
436
- version: resolved,
437
- yarn_major: 0,
438
- })
439
- }
440
- other => bail!("Unknown package manager: \"{other}\". Supported: npm, pnpm, yarn-classic, yarn-modern, bun, deno"),
441
- }
442
- }
443
-
444
- fn set(items: &[&'static str]) -> BTreeSet<&'static str> {
445
- items.iter().copied().collect()
446
- }
447
-
448
- fn resolve_installed_bin(
449
- bin_path: Option<&str>,
450
- version: Option<&str>,
451
- package_name: &str,
452
- default_bin: &str,
453
- bin_rel: &str,
454
- state: &mut RunState,
455
- ) -> Result<String> {
456
- if let Some(bin) = bin_path {
457
- return Ok(bin.to_string());
458
- }
459
- if let Some(version) = version {
460
- return install_package_bin(
461
- &format!("{package_name}@{version}"),
462
- &format!("node_modules/.bin/{bin_rel}"),
463
- state,
464
- );
465
- }
466
- Ok(default_bin.to_string())
467
- }
468
-
469
- fn resolve_yarn_classic_bin(state: &mut RunState) -> Result<String> {
470
- if let Ok(output) = command_output("which", &["yarn"], None, &[], false, None) {
471
- let bin = output.stdout.trim();
472
- if !bin.is_empty() {
473
- let version = detect_version_with_env(bin, &[], yarn_base_env())
474
- .unwrap_or_else(|_| "unknown".to_string());
475
- if parse_major(&version) == 1 {
476
- return Ok(bin.to_string());
477
- }
478
- }
479
- }
480
- install_package_bin("yarn@1", "node_modules/.bin/yarn", state)
481
- }
482
-
483
- fn resolve_yarn_modern_bin(state: &mut RunState) -> Result<String> {
484
- if let Ok(output) = command_output("which", &["yarn"], None, &[], false, None) {
485
- let bin = output.stdout.trim();
486
- if !bin.is_empty() {
487
- let version = detect_version_with_env(bin, &[], yarn_base_env())
488
- .unwrap_or_else(|_| "unknown".to_string());
489
- if parse_major(&version) >= 2 {
490
- return Ok(bin.to_string());
491
- }
492
- }
493
- }
494
- install_package_bin(
495
- "@yarnpkg/cli-dist@4",
496
- "node_modules/@yarnpkg/cli-dist/bin/yarn.js",
497
- state,
498
- )
499
- }
500
-
501
- fn install_package_bin(package: &str, bin_rel: &str, state: &mut RunState) -> Result<String> {
502
- let dir = tempfile::tempdir().context("failed to create package-manager install dir")?;
503
- let path = dir.path().to_path_buf();
504
- let prefix = path.to_string_lossy().to_string();
505
- command_output(
506
- "npm",
507
- &[
508
- "install",
509
- "--prefix",
510
- &prefix,
511
- package,
512
- "--loglevel=error",
513
- "--min-release-age=0",
514
- ],
515
- None,
516
- &[],
517
- false,
518
- None,
519
- )
520
- .with_context(|| format!("failed to install {package}"))?;
521
- let bin = path.join(bin_rel).to_string_lossy().to_string();
522
- let installed = detect_version(&bin, &[]).unwrap_or_else(|_| "unknown".to_string());
523
- println!(" Auto-installed {} {}", package_label(package), installed);
524
- state.temp_dirs.push(dir);
525
- Ok(bin)
526
- }
527
-
528
- fn package_label(package: &str) -> &str {
529
- package.rsplit_once('@').map_or(
530
- package,
531
- |(name, _)| {
532
- if name.is_empty() {
533
- package
534
- } else {
535
- name
536
- }
537
- },
538
- )
539
- }
540
-
541
- fn detect_version(bin: &str, args: &[&str]) -> Result<String> {
542
- detect_version_with_env(bin, args, Vec::new())
543
- }
544
-
545
- fn detect_version_with_env(
546
- bin: &str,
547
- args: &[&str],
548
- envs: Vec<(&'static str, &'static str)>,
549
- ) -> Result<String> {
550
- let mut full_args = Vec::from(args);
551
- full_args.push("--version");
552
- Ok(command_output(bin, &full_args, None, &envs, false, None)?
553
- .stdout
554
- .trim()
555
- .to_string())
556
- }
557
-
558
- fn parse_major(version: &str) -> u64 {
559
- version
560
- .split('.')
561
- .next()
562
- .and_then(|part| part.parse().ok())
563
- .unwrap_or(0)
564
- }
565
-
566
- fn yarn_base_env() -> Vec<(&'static str, &'static str)> {
567
- vec![("COREPACK_ENABLE_STRICT", "0"), ("YARN_IGNORE_PATH", "1")]
568
- }
569
-
570
- fn yarn_env(adapter: &Adapter) -> Vec<(&'static str, &'static str)> {
571
- let mut envs = yarn_base_env();
572
- if adapter.yarn_major >= 4 {
573
- envs.push(("YARN_ENABLE_HARDENED_MODE", "0"));
574
- envs.push(("YARN_NPM_MINIMAL_AGE_GATE", "0"));
575
- }
576
- envs
577
- }
578
-
579
- fn command_output(
580
- bin: &str,
581
- args: &[&str],
582
- cwd: Option<&Path>,
583
- envs: &[(&str, &str)],
584
- verbose: bool,
585
- timeout: Option<Duration>,
586
- ) -> Result<ExecOutput> {
587
- if verbose {
588
- let cwd_label = cwd
589
- .and_then(|path| path.file_name())
590
- .and_then(|name| name.to_str())
591
- .map(|name| format!(" {DIM}(cwd: {name}){RESET}"))
592
- .unwrap_or_default();
593
- println!(
594
- " {CYAN}${RESET} {} {}{}",
595
- short_bin(bin),
596
- args.join(" "),
597
- cwd_label
598
- );
599
- }
600
-
601
- let started = Instant::now();
602
- let mut command = Command::new(bin);
603
- command.args(args);
604
- command.stdout(Stdio::piped());
605
- command.stderr(Stdio::piped());
606
- if let Some(cwd) = cwd {
607
- command.current_dir(cwd);
608
- }
609
- for (key, value) in envs {
610
- command.env(key, value);
611
- }
612
- let mut child = command
613
- .spawn()
614
- .with_context(|| format!("failed to start command: {bin} {}", args.join(" ")))?;
615
- let status = loop {
616
- if let Some(status) = child
617
- .try_wait()
618
- .with_context(|| format!("failed waiting for command: {bin} {}", args.join(" ")))?
619
- {
620
- break status;
621
- }
622
- if let Some(limit) = timeout {
623
- if started.elapsed() >= limit {
624
- let _ = child.kill();
625
- let _ = child.wait();
626
- bail!(
627
- "Running \"{} {}\" timed out after {}",
628
- bin,
629
- args.join(" "),
630
- format_duration(limit)
631
- );
632
- }
633
- }
634
- std::thread::sleep(Duration::from_millis(25));
635
- };
636
- let mut stdout = String::new();
637
- let mut stderr = String::new();
638
- if let Some(mut pipe) = child.stdout.take() {
639
- pipe.read_to_string(&mut stdout)
640
- .context("failed to read command stdout")?;
641
- }
642
- if let Some(mut pipe) = child.stderr.take() {
643
- pipe.read_to_string(&mut stderr)
644
- .context("failed to read command stderr")?;
645
- }
646
-
647
- if verbose {
648
- let status_label = if status.success() {
649
- format!("{GREEN}ok{RESET}")
650
- } else {
651
- format!("{RED}exit {}{RESET}", status.code().unwrap_or(-1))
652
- };
653
- println!(
654
- " {DIM} -> {status_label} {DIM}({}){RESET}",
655
- format_duration(started.elapsed())
656
- );
657
- }
658
-
659
- if status.success() {
660
- Ok(ExecOutput { stdout, stderr })
661
- } else {
662
- bail!(
663
- "Running \"{} {}\" returned error code {}...\n\nSTDOUT:\n{}\n\nSTDERR:\n{}\n",
664
- bin,
665
- args.join(" "),
666
- status.code().unwrap_or(-1),
667
- stdout,
668
- stderr
669
- )
670
- }
671
- }
672
-
673
- fn short_bin(bin: &str) -> &str {
674
- Path::new(bin)
675
- .file_name()
676
- .and_then(|name| name.to_str())
677
- .unwrap_or(bin)
678
- }
679
-
680
- fn exec_adapter(
681
- ctx: &TestContext<'_>,
682
- cwd: Option<&Path>,
683
- args: Vec<String>,
684
- ) -> Result<ExecOutput> {
685
- let args = normalize_adapter_args(&ctx.adapter, args);
686
- let refs = args.iter().map(String::as_str).collect::<Vec<_>>();
687
- let envs = match ctx.adapter.kind {
688
- AdapterType::YarnClassic | AdapterType::YarnModern => yarn_env(&ctx.adapter),
689
- _ => Vec::new(),
690
- };
691
- command_output(
692
- &ctx.adapter.bin,
693
- &refs,
694
- cwd,
695
- &envs,
696
- ctx.state.verbose,
697
- Some(remaining_test_time(ctx)?),
698
- )
699
- }
700
-
701
- fn remaining_test_time(ctx: &TestContext<'_>) -> Result<Duration> {
702
- ctx.deadline
703
- .checked_duration_since(Instant::now())
704
- .ok_or_else(|| anyhow!("Test timed out after {}", format_duration(ctx.timeout)))
705
- }
706
-
707
- fn normalize_adapter_args(adapter: &Adapter, args: Vec<String>) -> Vec<String> {
708
- match adapter.kind {
709
- AdapterType::Pnpm if parse_major(&adapter.version) >= 11 => {
710
- let cmd = args.first().map(String::as_str).unwrap_or_default();
711
- let mut filtered = if matches!(cmd, "deprecate" | "unpublish" | "publish" | "dist-tag")
712
- {
713
- args.into_iter()
714
- .filter(|arg| arg != "--json" && !arg.starts_with("--loglevel"))
715
- .collect::<Vec<_>>()
716
- } else if cmd == "version" {
717
- args.into_iter()
718
- .filter(|arg| {
719
- !arg.starts_with("--registry")
720
- && !arg.starts_with("--loglevel")
721
- && !arg.starts_with("--no-git-tag-version")
722
- })
723
- .collect::<Vec<_>>()
724
- } else {
725
- args
726
- };
727
- if filtered.first().map(String::as_str) == Some("deprecate")
728
- && filtered.get(2).map(String::as_str) == Some("")
729
- {
730
- let mut updated = vec!["undeprecate".to_string(), filtered[1].clone()];
731
- updated.extend(filtered.drain(3..));
732
- filtered = updated;
733
- }
734
- filtered
735
- }
736
- AdapterType::YarnModern => {
737
- let cmd = args.first().cloned().unwrap_or_default();
738
- let mut rest = args
739
- .into_iter()
740
- .skip(1)
741
- .filter(|arg| !arg.starts_with("--registry"))
742
- .collect::<Vec<_>>();
743
- match cmd.as_str() {
744
- "publish" => {
745
- rest.retain(|arg| arg != "--json");
746
- let mut out = vec!["npm".to_string(), "publish".to_string()];
747
- out.extend(rest);
748
- out
749
- }
750
- "info" => prepend(&["npm", "info"], rest),
751
- "ping" => prepend(&["npm", "ping"], rest),
752
- "deprecate" => prepend(&["npm", "deprecate"], rest),
753
- "login" => prepend(&["npm", "login"], rest),
754
- "whoami" => prepend(&["npm", "whoami"], rest),
755
- _ => {
756
- let mut out = vec![cmd.to_string()];
757
- out.extend(rest);
758
- out
759
- }
760
- }
761
- }
762
- AdapterType::Bun if args.first().map(String::as_str) == Some("info") => {
763
- let pkg = args.get(1).cloned().unwrap_or_default();
764
- let cleaned = args
765
- .into_iter()
766
- .skip(2)
767
- .filter(|arg| arg != "--registry" && !arg.starts_with("http"))
768
- .collect::<Vec<_>>();
769
- let mut out = vec!["info".to_string(), pkg];
770
- out.extend(cleaned);
771
- out
772
- }
773
- AdapterType::Deno if args.first().map(String::as_str) == Some("info") => {
774
- let pkg = args.get(1).cloned().unwrap_or_default();
775
- let mut out = vec![
776
- "info".to_string(),
777
- format!("npm:{pkg}"),
778
- "--node-modules-dir=auto".to_string(),
779
- ];
780
- out.extend(args.into_iter().skip(2).filter(|arg| arg != "--json"));
781
- out
782
- }
783
- AdapterType::Deno if args.first().map(String::as_str) == Some("install") => {
784
- vec!["install".to_string()]
785
- }
786
- _ => args,
787
- }
788
- }
789
-
790
- fn prepend(prefix: &[&str], rest: Vec<String>) -> Vec<String> {
791
- let mut out = prefix
792
- .iter()
793
- .map(|item| item.to_string())
794
- .collect::<Vec<_>>();
795
- out.extend(rest);
796
- out
797
- }
798
-
799
- fn registry_arg(adapter: &Adapter, url: &str) -> Vec<String> {
800
- match adapter.kind {
801
- AdapterType::Deno | AdapterType::YarnModern => Vec::new(),
802
- _ => vec!["--registry".to_string(), url.to_string()],
803
- }
804
- }
805
-
806
- fn prepare_project(
807
- ctx: &mut TestContext<'_>,
808
- package_name: &str,
809
- version: &str,
810
- dependencies: BTreeMap<String, String>,
811
- dev_dependencies: BTreeMap<String, String>,
812
- ) -> Result<PathBuf> {
813
- let dir = create_temp_dir(ctx.state, package_name)?;
814
- let path = dir.path().to_path_buf();
815
- fs::write(
816
- path.join("package.json"),
817
- package_json(package_name, version, dependencies, dev_dependencies)?,
818
- )?;
819
- fs::write(path.join("README.md"), readme(package_name))?;
820
-
821
- if ctx.adapter.kind == AdapterType::YarnModern {
822
- let yaml = yarn_config(&ctx.registry_url, Some(&ctx.token), ctx.adapter.yarn_major)?;
823
- fs::write(path.join(".yarnrc.yml"), yaml)?;
824
- } else {
825
- let npmrc = format!(
826
- "{}:_authToken={}\nregistry={}\nmin-release-age=0\nminimum-release-age=0",
827
- npm_auth_scope(&ctx.registry_url)?,
828
- ctx.token,
829
- ctx.registry_url
830
- );
831
- fs::write(path.join(".npmrc"), npmrc)?;
832
- if ctx.adapter.kind == AdapterType::Pnpm {
833
- fs::write(
834
- path.join("pnpm-workspace.yaml"),
835
- "packages:\n - .\nminimumReleaseAge: 0\n",
836
- )?;
837
- }
838
- }
839
- Ok(path)
840
- }
841
-
842
- fn package_json(
843
- package_name: &str,
844
- version: &str,
845
- dependencies: BTreeMap<String, String>,
846
- dev_dependencies: BTreeMap<String, String>,
847
- ) -> Result<String> {
848
- Ok(serde_json::to_string(&json!({
849
- "name": package_name,
850
- "version": version,
851
- "description": "some cool project",
852
- "main": "index.js",
853
- "scripts": { "test": "echo exit 1" },
854
- "dependencies": dependencies,
855
- "devDependencies": dev_dependencies,
856
- "keywords": ["foo", "bar"],
857
- "author": "Verdaccio E2E <verdaccio@example.org>",
858
- "license": "MIT"
859
- }))?)
860
- }
861
-
862
- fn readme(package_name: &str) -> String {
863
- format!("\n # My README {package_name}\n\n some text\n\n ## subtitle\n\n more text\n ")
864
- }
865
-
866
- fn yarn_config(registry: &str, token: Option<&str>, major: u64) -> Result<String> {
867
- let mut value = serde_yaml::Mapping::new();
868
- value.insert("npmRegistryServer".into(), registry.into());
869
- value.insert("enableImmutableInstalls".into(), false.into());
870
- value.insert("unsafeHttpWhitelist".into(), vec!["localhost"].into());
871
- if major >= 4 {
872
- value.insert("enableHardenedMode".into(), false.into());
873
- value.insert("npmMinimalAgeGate".into(), 0.into());
874
- }
875
- if let Some(token) = token {
876
- let mut registry_config = serde_yaml::Mapping::new();
877
- registry_config.insert("npmAlwaysAuth".into(), true.into());
878
- registry_config.insert("npmAuthToken".into(), token.into());
879
- let mut registries = serde_yaml::Mapping::new();
880
- registries.insert(registry.into(), registry_config.into());
881
- value.insert("npmRegistries".into(), registries.into());
882
- }
883
- Ok(serde_yaml::to_string(&value)?)
884
- }
885
-
886
- fn create_temp_dir<'a>(state: &'a mut RunState, prefix: &str) -> Result<&'a TempDir> {
887
- let sanitized = prefix
888
- .chars()
889
- .map(|c| {
890
- if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
891
- c
892
- } else {
893
- '-'
894
- }
895
- })
896
- .collect::<String>();
897
- let dir = Builder::new()
898
- .prefix(&format!("verdaccio-e2e-{sanitized}-"))
899
- .tempdir()
900
- .context("failed to create temp dir")?;
901
- state.temp_dirs.push(dir);
902
- Ok(state.temp_dirs.last().expect("temp dir was just pushed"))
903
- }
904
-
905
- fn ping_registry(registry_url: &str) -> bool {
906
- Client::new()
907
- .get(format!("{}/-/ping", registry_url.trim_end_matches('/')))
908
- .timeout(Duration::from_secs(10))
909
- .send()
910
- .map(|resp| resp.status().is_success())
911
- .unwrap_or(false)
912
- }
913
-
914
- fn create_user(registry_url: &str, user: &str, password: &str) -> Result<String> {
915
- let url = format!(
916
- "{}/-/user/org.couchdb.user:{}",
917
- registry_url.trim_end_matches('/'),
918
- user
919
- );
920
- let body = json!({
921
- "name": user,
922
- "password": password,
923
- "_id": format!("org.couchdb.user:{user}"),
924
- "type": "user",
925
- "roles": [],
926
- "date": unix_millis_string(),
927
- });
928
- let response = Client::new()
929
- .put(url)
930
- .header("Content-Type", "application/json")
931
- .json(&body)
932
- .send()
933
- .context("failed to create registry user")?;
934
- let status = response.status();
935
- let body: Value = response.json().unwrap_or(Value::Null);
936
- if matches!(status.as_u16(), 200 | 201 | 409) {
937
- if let Some(token) = body.get("token").and_then(Value::as_str) {
938
- return Ok(token.to_string());
939
- }
940
- }
941
- bail!("Failed to create/login user \"{user}\" on {registry_url}: {status} {body}")
942
- }
943
-
944
- fn unix_millis_string() -> String {
945
- format!("{}", now_millis())
946
- }
947
-
948
- fn now_millis() -> u128 {
949
- SystemTime::now()
950
- .duration_since(UNIX_EPOCH)
951
- .unwrap_or_default()
952
- .as_millis()
953
- }
954
-
955
- fn run_id() -> String {
956
- let counter = RUN_COUNTER.fetch_add(1, Ordering::Relaxed);
957
- format!("{:x}-{counter:x}", now_millis())
958
- }
959
-
960
- fn npm_auth_scope(registry_url: &str) -> Result<String> {
961
- let url = Url::parse(registry_url)
962
- .with_context(|| format!("failed to parse registry URL: {registry_url}"))?;
963
- let host = url
964
- .host_str()
965
- .ok_or_else(|| anyhow!("registry URL has no host: {registry_url}"))?;
966
- let port = url.port().map_or(String::new(), |port| format!(":{port}"));
967
- let path = url.path().trim_end_matches('/');
968
- Ok(format!("//{host}{port}{path}/"))
969
- }
970
-
971
- fn sub_test(
972
- ctx: &mut TestContext<'_>,
973
- label: &'static str,
974
- f: impl FnOnce(&mut TestContext<'_>) -> Result<()>,
975
- ) -> Result<()> {
976
- let started = Instant::now();
977
- match f(ctx) {
978
- Ok(()) => {
979
- let duration = started.elapsed();
980
- ctx.sub_results.push(SubTestResult {
981
- label,
982
- passed: true,
983
- duration,
984
- error: None,
985
- });
986
- report_sub_test(label, true, duration);
987
- Ok(())
988
- }
989
- Err(err) => {
990
- let duration = started.elapsed();
991
- let error = format!("{err:#}");
992
- ctx.sub_results.push(SubTestResult {
993
- label,
994
- passed: false,
995
- duration,
996
- error: Some(error.clone()),
997
- });
998
- report_sub_test(label, false, duration);
999
- Err(anyhow!(error))
1000
- }
1001
- }
1002
- }
1003
-
1004
- fn prepare_deprecate_project(
1005
- ctx: &mut TestContext<'_>,
1006
- pkg: &str,
1007
- version: &str,
1008
- ) -> Result<PathBuf> {
1009
- let temp = prepare_project(ctx, pkg, version, BTreeMap::new(), BTreeMap::new())?;
1010
- if ctx.adapter.kind == AdapterType::YarnModern {
1011
- import_plugin(ctx, &temp, "npm-deprecate")?;
1012
- }
1013
- Ok(temp)
1014
- }
1015
-
1016
- fn publish_pkg(ctx: &TestContext<'_>, temp: &Path) -> Result<()> {
1017
- publish_pkg_extra(ctx, temp, Vec::new())
1018
- }
1019
-
1020
- fn publish_pkg_extra(ctx: &TestContext<'_>, temp: &Path, extra: Vec<String>) -> Result<()> {
1021
- if ctx.adapter.kind == AdapterType::YarnModern {
1022
- exec_adapter(ctx, Some(temp), vec!["install".into()])?;
1023
- }
1024
- let mut args = vec!["publish".into()];
1025
- args.extend(extra);
1026
- args.extend(registry_arg(&ctx.adapter, &ctx.registry_url));
1027
- exec_adapter(ctx, Some(temp), args)?;
1028
- Ok(())
1029
- }
1030
-
1031
- fn publish_seed(
1032
- ctx: &mut TestContext<'_>,
1033
- pkg: &str,
1034
- version: &str,
1035
- deps: BTreeMap<String, String>,
1036
- ) -> Result<()> {
1037
- let temp = prepare_project(ctx, pkg, version, deps, BTreeMap::new())?;
1038
- publish_pkg(ctx, &temp)
1039
- }
1040
-
1041
- fn deprecate_pkg(
1042
- ctx: &TestContext<'_>,
1043
- temp: &Path,
1044
- package_version: &str,
1045
- message: &str,
1046
- ) -> Result<()> {
1047
- let mut args = vec!["deprecate".into(), package_version.into(), message.into()];
1048
- if ctx.adapter.kind != AdapterType::YarnModern {
1049
- args.push("--json".into());
1050
- }
1051
- args.extend(registry_arg(&ctx.adapter, &ctx.registry_url));
1052
- exec_adapter(ctx, Some(temp), args)?;
1053
- Ok(())
1054
- }
1055
-
1056
- fn info_pkg(ctx: &TestContext<'_>, temp: &Path, pkg: &str) -> Result<Value> {
1057
- let mut args = vec!["info".into(), pkg.into(), "--json".into()];
1058
- args.extend(registry_arg(&ctx.adapter, &ctx.registry_url));
1059
- let resp = exec_adapter(ctx, Some(temp), args)?;
1060
- if ctx.adapter.kind == AdapterType::YarnClassic {
1061
- for line in resp.stdout.lines() {
1062
- if let Ok(value) = serde_json::from_str::<Value>(line) {
1063
- if value.get("type").and_then(Value::as_str) == Some("inspect") {
1064
- return Ok(normalize_info(
1065
- value.get("data").cloned().unwrap_or(Value::Null),
1066
- ));
1067
- }
1068
- }
1069
- }
1070
- }
1071
- parse_info_json(&resp.stdout)
1072
- }
1073
-
1074
- fn parse_info_json(stdout: &str) -> Result<Value> {
1075
- Ok(normalize_info(serde_json::from_str(stdout)?))
1076
- }
1077
-
1078
- fn normalize_info(value: Value) -> Value {
1079
- if let Some(array) = value.as_array() {
1080
- array.last().cloned().unwrap_or(Value::Null)
1081
- } else {
1082
- value
1083
- }
1084
- }
1085
-
1086
- fn bump_version(ctx: &TestContext<'_>, temp: &Path, bump: &str) -> Result<()> {
1087
- if ctx.adapter.kind == AdapterType::YarnModern {
1088
- let path = temp.join("package.json");
1089
- let mut pkg: Value = serde_json::from_str(&fs::read_to_string(&path)?)?;
1090
- let current = pkg
1091
- .get("version")
1092
- .and_then(Value::as_str)
1093
- .unwrap_or("1.0.0");
1094
- let mut parts = current
1095
- .split('.')
1096
- .map(|p| p.parse::<u64>().unwrap_or(0))
1097
- .collect::<Vec<_>>();
1098
- while parts.len() < 3 {
1099
- parts.push(0);
1100
- }
1101
- match bump {
1102
- "major" => {
1103
- parts[0] += 1;
1104
- parts[1] = 0;
1105
- parts[2] = 0;
1106
- }
1107
- _ => {
1108
- parts[1] += 1;
1109
- parts[2] = 0;
1110
- }
1111
- }
1112
- pkg["version"] = format!("{}.{}.{}", parts[0], parts[1], parts[2]).into();
1113
- fs::write(path, serde_json::to_string_pretty(&pkg)?)?;
1114
- } else {
1115
- let mut args = vec![
1116
- "version".into(),
1117
- bump.into(),
1118
- "--no-git-tag-version".into(),
1119
- "--loglevel=info".into(),
1120
- ];
1121
- args.extend(registry_arg(&ctx.adapter, &ctx.registry_url));
1122
- exec_adapter(ctx, Some(temp), args)?;
1123
- }
1124
- Ok(())
1125
- }
1126
-
1127
- fn prepare_login_project(ctx: &mut TestContext<'_>, name: &str) -> Result<PathBuf> {
1128
- let temp = create_temp_dir(ctx.state, name)?.path().to_path_buf();
1129
- fs::write(
1130
- temp.join(".yarnrc.yml"),
1131
- yarn_config(&ctx.registry_url, None, ctx.adapter.yarn_major)?,
1132
- )?;
1133
- fs::write(
1134
- temp.join("package.json"),
1135
- serde_json::to_string(&json!({ "name": name, "version": "1.0.0" }))?,
1136
- )?;
1137
- import_plugin(ctx, &temp, "npm-login")?;
1138
- Ok(temp)
1139
- }
1140
-
1141
- fn yarn_login(
1142
- ctx: &TestContext<'_>,
1143
- cwd: &Path,
1144
- user: &str,
1145
- password: &str,
1146
- email: &str,
1147
- ) -> Result<ExecOutput> {
1148
- exec_adapter(
1149
- ctx,
1150
- Some(cwd),
1151
- vec![
1152
- "login".into(),
1153
- "--auth-type=legacy".into(),
1154
- format!("--user={user}"),
1155
- format!("--password={password}"),
1156
- format!("--email={email}"),
1157
- ],
1158
- )
1159
- }
1160
-
1161
- fn yarn_whoami(ctx: &TestContext<'_>, cwd: &Path) -> Result<String> {
1162
- Ok(exec_adapter(ctx, Some(cwd), vec!["whoami".into()])?.stdout)
1163
- }
1164
-
1165
- fn import_plugin(ctx: &TestContext<'_>, cwd: &Path, plugin_name: &str) -> Result<()> {
1166
- if ctx.adapter.kind != AdapterType::YarnModern {
1167
- return Ok(());
1168
- }
1169
- let tmp = tempfile::tempdir()?;
1170
- let tmp_path = tmp.path().to_string_lossy().to_string();
1171
- let pkg = format!("@verdaccio/yarn-plugin-{plugin_name}");
1172
- command_output(
1173
- "npm",
1174
- &["pack", &pkg, "--pack-destination", &tmp_path],
1175
- None,
1176
- &[],
1177
- false,
1178
- None,
1179
- )?;
1180
- let tgz = fs::read_dir(tmp.path())?
1181
- .filter_map(Result::ok)
1182
- .map(|entry| entry.path())
1183
- .find(|path| path.extension().and_then(|ext| ext.to_str()) == Some("tgz"))
1184
- .ok_or_else(|| anyhow!("failed to find packed yarn plugin"))?;
1185
- let tgz_str = tgz.to_string_lossy().to_string();
1186
- command_output(
1187
- "tar",
1188
- &["xzf", &tgz_str, "-C", &tmp_path],
1189
- None,
1190
- &[],
1191
- false,
1192
- None,
1193
- )?;
1194
- let bundle = tmp
1195
- .path()
1196
- .join(format!("package/bundles/@yarnpkg/plugin-{plugin_name}.js"));
1197
- let bundle = bundle.to_string_lossy().to_string();
1198
- exec_adapter(
1199
- ctx,
1200
- Some(cwd),
1201
- vec!["plugin".into(), "import".into(), bundle],
1202
- )?;
1203
- Ok(())
1204
- }
1205
-
1206
- fn prepare_cooldown_consumer(
1207
- ctx: &mut TestContext<'_>,
1208
- name: &str,
1209
- deps: BTreeMap<String, String>,
1210
- ) -> Result<PathBuf> {
1211
- let temp = prepare_project(ctx, name, "1.0.0", deps, BTreeMap::new())?;
1212
- fs::write(
1213
- temp.join(".npmrc"),
1214
- format!(
1215
- "{}:_authToken={}\nregistry={}",
1216
- npm_auth_scope(&ctx.registry_url)?,
1217
- ctx.token,
1218
- ctx.registry_url
1219
- ),
1220
- )?;
1221
- fs::write(
1222
- temp.join("pnpm-workspace.yaml"),
1223
- "packages:\n - .\nminimumReleaseAge: 10080\nminimumReleaseAgeExclude:\n - '@verdaccio/*'\n - 'verdaccio-*'\n",
1224
- )?;
1225
- Ok(temp)
1226
- }
1227
-
1228
- fn is_pnpm_minimum_release_age_capable(adapter: &Adapter) -> bool {
1229
- if adapter.kind != AdapterType::Pnpm {
1230
- return false;
1231
- }
1232
- let mut parts = adapter
1233
- .version
1234
- .split('.')
1235
- .filter_map(|part| part.parse::<u64>().ok());
1236
- let major = parts.next().unwrap_or(0);
1237
- let minor = parts.next().unwrap_or(0);
1238
- major == 11 && minor >= 1
1239
- }
1240
-
1241
- fn assert_true(condition: bool, message: &str) -> Result<()> {
1242
- if condition {
1243
- Ok(())
1244
- } else {
1245
- bail!("{message}")
1246
- }
1247
- }
1248
-
1249
- fn assert_contains(haystack: &str, needle: &str) -> Result<()> {
1250
- assert_true(
1251
- haystack.contains(needle),
1252
- &format!("Expected output to contain \"{needle}\" but got \"{haystack}\""),
1253
- )
1254
- }
1255
-
1256
- fn assert_eq_str(actual: &str, expected: &str) -> Result<()> {
1257
- if actual == expected {
1258
- Ok(())
1259
- } else {
1260
- bail!("Expected \"{expected}\" but got \"{actual}\"")
1261
- }
1262
- }
1263
-
1264
- fn assert_eq_json<T: Into<Value>>(
1265
- actual: Option<&Value>,
1266
- expected: T,
1267
- message: &str,
1268
- ) -> Result<()> {
1269
- let expected = expected.into();
1270
- if actual == Some(&expected) {
1271
- Ok(())
1272
- } else {
1273
- bail!(
1274
- "{message}: expected {expected}, got {}",
1275
- actual.cloned().unwrap_or(Value::Null)
1276
- )
1277
- }
1278
- }
1279
-
1280
- fn report_test_result(result: &TestResult) {
1281
- if result.passed {
1282
- if result.sub_tests.is_empty() {
1283
- println!(
1284
- " {GREEN}✓{RESET} {} {DIM}{}{RESET}",
1285
- result.name,
1286
- format_duration(result.duration)
1287
- );
1288
- } else {
1289
- println!(
1290
- " {GREEN}PASS{RESET} {} {DIM}{}{RESET}",
1291
- result.name,
1292
- format_duration(result.duration)
1293
- );
1294
- }
1295
- } else {
1296
- println!(
1297
- " {RED}FAIL{RESET} {} {DIM}{}{RESET}",
1298
- result.name,
1299
- format_duration(result.duration)
1300
- );
1301
- if let Some(error) = &result.error {
1302
- for line in error.lines() {
1303
- println!(" {RED}{line}{RESET}");
1304
- }
1305
- if env::var_os("GITHUB_ACTIONS").is_some() {
1306
- let first = error.lines().next().unwrap_or(error);
1307
- println!("::error title=E2E {}::{first}", result.name);
1308
- }
1309
- }
1310
- }
1311
- }
1312
-
1313
- fn report_sub_test(label: &str, passed: bool, duration: Duration) {
1314
- let icon = if passed {
1315
- format!("{BOLD}{GREEN}✓{RESET}")
1316
- } else {
1317
- format!("{BOLD}{RED}✗{RESET}")
1318
- };
1319
- println!(
1320
- "\n {icon} {BOLD}{label}{RESET} {DIM}{}{RESET}\n",
1321
- format_duration(duration)
1322
- );
1323
- }
1324
-
1325
- fn report_summary(results: &[SuiteResult]) -> Result<()> {
1326
- println!("\n{BOLD}{}{RESET}", "=".repeat(50));
1327
- println!("{BOLD}Summary{RESET}\n");
1328
- let mut total_passed = 0usize;
1329
- let mut total_failed = 0usize;
1330
- let mut total_skipped = 0usize;
1331
- let mut total_duration = Duration::ZERO;
1332
- for suite in results {
1333
- let color = if suite.failed > 0 { RED } else { GREEN };
1334
- let icon = if suite.failed > 0 { "x" } else { "v" };
1335
- println!(
1336
- " {color}{icon}{RESET} {}: {GREEN}{} passed{RESET}, {RED}{} failed{RESET}, {YELLOW}{} skipped{RESET} {DIM}({}){RESET}",
1337
- suite.adapter,
1338
- suite.passed,
1339
- suite.failed,
1340
- suite.skipped,
1341
- format_duration(suite.duration)
1342
- );
1343
- total_passed += suite.passed;
1344
- total_failed += suite.failed;
1345
- total_skipped += suite.skipped;
1346
- total_duration += suite.duration;
1347
- }
1348
- println!(
1349
- "\n Total: {GREEN}{total_passed} passed{RESET}, {RED}{total_failed} failed{RESET}, {YELLOW}{total_skipped} skipped{RESET} {DIM}({}){RESET}",
1350
- format_duration(total_duration)
1351
- );
1352
- println!("{BOLD}{}{RESET}\n", "=".repeat(50));
1353
- if let Some(summary) = env::var_os("GITHUB_STEP_SUMMARY") {
1354
- write_github_summary(Path::new(&summary), results)?;
1355
- }
1356
- Ok(())
1357
- }
1358
-
1359
- fn write_github_summary(path: &Path, results: &[SuiteResult]) -> Result<()> {
1360
- let mut file = fs::OpenOptions::new()
1361
- .create(true)
1362
- .append(true)
1363
- .open(path)?;
1364
- writeln!(file, "## E2E Test Results\n")?;
1365
- for suite in results {
1366
- let status = if suite.failed > 0 { "❌" } else { "✅" };
1367
- writeln!(file, "### {status} {}\n", suite.adapter)?;
1368
- writeln!(file, "| Test | Status | Duration |")?;
1369
- writeln!(file, "|------|--------|----------|")?;
1370
- for test in &suite.tests {
1371
- let icon = if test.passed { "✅" } else { "❌" };
1372
- writeln!(
1373
- file,
1374
- "| {} | {} | {} |",
1375
- test.name,
1376
- icon,
1377
- format_duration(test.duration)
1378
- )?;
1379
- for sub in &test.sub_tests {
1380
- let icon = if sub.passed { "✅" } else { "❌" };
1381
- let label = if sub.passed {
1382
- sub.label.to_string()
1383
- } else {
1384
- format!("{}: {}", sub.label, sub.error.as_deref().unwrap_or(""))
1385
- };
1386
- writeln!(
1387
- file,
1388
- "| &nbsp;&nbsp;&nbsp;&nbsp;{} | {} | {} |",
1389
- label,
1390
- icon,
1391
- format_duration(sub.duration)
1392
- )?;
1393
- }
1394
- }
1395
- writeln!(file)?;
1396
- }
1397
- let total_passed: usize = results.iter().map(|suite| suite.passed).sum();
1398
- let total_failed: usize = results.iter().map(|suite| suite.failed).sum();
1399
- let total_skipped: usize = results.iter().map(|suite| suite.skipped).sum();
1400
- let emoji = if total_failed > 0 { "❌" } else { "✅" };
1401
- writeln!(file, "{emoji} **{total_passed} passed**, **{total_failed} failed**, **{total_skipped} skipped**\n")?;
1402
- Ok(())
1403
- }
1404
-
1405
- fn format_duration(duration: Duration) -> String {
1406
- if duration < Duration::from_secs(1) {
1407
- format!("{}ms", duration.as_millis())
1408
- } else {
1409
- format!("{:.1}s", duration.as_secs_f64())
1410
- }
1411
- }