Skip to content

Commit c137722

Browse files
feat: add support for private packages (#327)
* feat(lock): add support for private packages * refactor(error): rename error variant * feat(core): support installing private package * feat(registry): authenticate all requests if logged in * feat: save correct lockfile entry for private projects * test(core): add install private package test * docs: doc_auto_cfg merged into doc_cfg Ref: rust-lang/rust#138907 * test: add integration test
1 parent dba64cf commit c137722

File tree

8 files changed

+428
-97
lines changed

8 files changed

+428
-97
lines changed

crates/commands/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! High-level commands for the Soldeer CLI
2-
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
2+
#![cfg_attr(docsrs, feature(doc_cfg))]
33
pub use crate::commands::{Args, Command};
44
use clap::builder::PossibleValue;
55
pub use clap_verbosity_flag::Verbosity;

crates/commands/tests/tests-install.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use mockito::Matcher;
12
use soldeer_commands::{Command, Verbosity, commands::install::Install, run};
23
use soldeer_core::{
34
config::{ConfigLocation, read_config_deps},
@@ -327,6 +328,68 @@ integrity = "f3c628f3e9eae4db14fe14f9ab29e49a0107c47b8ee956e4cee57b616b493fc2"
327328
mock.assert(); // download link was not called a second time
328329
}
329330

331+
#[tokio::test]
332+
async fn test_install_private_second_time() {
333+
let dir = testdir!();
334+
let contents = r#"[dependencies]
335+
test-private = "0.1.0"
336+
"#;
337+
fs::write(dir.join("soldeer.toml"), contents).unwrap();
338+
339+
// get zip file locally for mock
340+
let zip_file = download_file(
341+
"https://github.com/mario-eth/soldeer/archive/8585a7ec85a29889cec8d08f4770e15ec4795943.zip",
342+
&dir,
343+
"tmp",
344+
)
345+
.await
346+
.unwrap();
347+
348+
// serve the file with mock server
349+
let mut server = mockito::Server::new_async().await;
350+
let data = format!(
351+
r#"{{"data":[{{"created_at":"2025-09-28T12:36:09.526660Z","deleted":false,"id":"0440c261-8cdf-4738-9139-c4dc7b0c7f3e","internal_name":"test-private/0_1_0_28-09-2025_12:36:08_test-private.zip","private":true,"project_id":"14f419e7-2d64-49e4-86b9-b44b36627786","url":"{}/file.zip","version":"0.1.0"}}],"status":"success"}}"#,
352+
server.url()
353+
);
354+
server.mock("GET", "/file.zip").with_body_from_file(zip_file).create_async().await;
355+
server
356+
.mock("GET", "/api/v1/revision-cli")
357+
.match_query(Matcher::Any)
358+
.with_header("content-type", "application/json")
359+
.with_body(data)
360+
.create_async()
361+
.await;
362+
363+
let lock = r#"[[dependencies]]
364+
name = "test-private"
365+
version = "0.1.0"
366+
checksum = "94a73dbe106f48179ea39b00d42e5d4dd96fdc6252caa3a89ce7efdaec0b9468"
367+
integrity = "f3c628f3e9eae4db14fe14f9ab29e49a0107c47b8ee956e4cee57b616b493fc2"
368+
"#;
369+
fs::write(dir.join("soldeer.lock"), lock).unwrap();
370+
let cmd: Command = Install::builder().build().into();
371+
let res = async_with_vars(
372+
[
373+
("SOLDEER_API_URL", Some(server.url().as_str())),
374+
("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref())),
375+
],
376+
run(cmd.clone(), Verbosity::default()),
377+
)
378+
.await;
379+
assert!(res.is_ok(), "{res:?}");
380+
381+
// second install
382+
let res = async_with_vars(
383+
[
384+
("SOLDEER_API_URL", Some(server.url().as_str())),
385+
("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref())),
386+
],
387+
run(cmd, Verbosity::default()),
388+
)
389+
.await;
390+
assert!(res.is_ok(), "{res:?}");
391+
}
392+
330393
#[tokio::test]
331394
async fn test_install_add_existing_reinstall() {
332395
let dir = testdir!();

crates/core/src/auth.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ pub fn get_token() -> Result<String> {
4545
Ok(jwt)
4646
}
4747

48+
/// Get a header map with the bearer token set up if it exists
49+
pub fn get_auth_headers() -> Result<HeaderMap> {
50+
let mut headers: HeaderMap = HeaderMap::new();
51+
let Ok(token) = get_token() else {
52+
return Ok(headers);
53+
};
54+
let header_value =
55+
HeaderValue::from_str(&format!("Bearer {token}")).map_err(|_| AuthError::InvalidToken)?;
56+
headers.insert(AUTHORIZATION, header_value);
57+
Ok(headers)
58+
}
59+
4860
/// Save an access token in the login file
4961
pub fn save_token(token: &str) -> Result<PathBuf> {
5062
let token_path = login_file_path()?;

crates/core/src/errors.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ pub enum LockError {
185185
#[error("error generating soldeer.lock contents: {0}")]
186186
SerializeError(#[from] toml_edit::ser::Error),
187187

188-
#[error("lock entry does not match expected type")]
189-
TypeMismatch,
188+
#[error("lock entry does not match a valid format")]
189+
InvalidLockEntry,
190190

191191
#[error("missing `{field}` field in lock entry for {dep}")]
192192
MissingField { field: String, dep: String },
@@ -252,10 +252,13 @@ pub enum RegistryError {
252252
URLNotFound(String),
253253

254254
#[error(
255-
"project {0} not found, please check the dependency name (project name) or create a new project on https://soldeer.xyz"
255+
"project {0} not found. Private projects require to log in before install. Please check the dependency name (project name) or create a new project on https://soldeer.xyz"
256256
)]
257257
ProjectNotFound(String),
258258

259+
#[error("auth error: {0}")]
260+
AuthError(#[from] AuthError),
261+
259262
#[error("package {0} has no version")]
260263
NoVersion(String),
261264

0 commit comments

Comments
 (0)