upd-cli 0.1.5__tar.gz → 0.1.6__tar.gz
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.
- {upd_cli-0.1.5 → upd_cli-0.1.6}/CHANGELOG.md +21 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/Cargo.lock +1 -1
- {upd_cli-0.1.5 → upd_cli-0.1.6}/Cargo.toml +1 -1
- {upd_cli-0.1.5 → upd_cli-0.1.6}/PKG-INFO +1 -1
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/audit/mod.rs +13 -8
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/cli.rs +31 -0
- upd_cli-0.1.6/src/http.rs +516 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/lib.rs +1 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/main.rs +39 -6
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/crates_io.rs +17 -15
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/github_releases.rs +9 -7
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/go_proxy.rs +12 -10
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/mod.rs +2 -2
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/npm.rs +12 -9
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/nuget.rs +9 -7
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/pypi.rs +13 -11
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/rubygems.rs +9 -7
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/terraform.rs +18 -14
- {upd_cli-0.1.5 → upd_cli-0.1.6}/.mise.toml +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/.pre-commit-hooks.yaml +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/.rumdl.toml +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/LICENSE +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/Makefile +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/README.md +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/assets/logo-wide.svg +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/assets/logo.svg +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/pyproject.toml +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/rust-toolchain.toml +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/align.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/audit/cache.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/audit/cvss.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/cache.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/config.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/cooldown.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/interactive.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/lockfile.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/output.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/mock.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/registry/utils.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/csproj.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/gemfile.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/github_actions.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/mise.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/mod.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/npm_range.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/package_json.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/pre_commit.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/requirements.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/updater/terraform.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/version/compare.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/version/mod.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/version/pep440.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/version/semver_util.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/src/version/tag.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/audit_offline.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/audit_sarif.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/audit_severity.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/bump_filter.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/cooldown_e2e.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/discovery_no_ignore.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/exit_codes.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/fix_audit.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/format_json.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/help_text.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/interactive_tty.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/invalid_positional.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/no_args_scope.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/output_streams.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/tests/package_filter.rs +0 -0
- {upd_cli-0.1.5 → upd_cli-0.1.6}/vership.toml +0 -0
|
@@ -15,6 +15,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
## [0.1.6](https://github.com/rvben/upd/compare/v0.1.5...v0.1.6) - 2026-04-29
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **http**: wire TLS init into networked subcommand entry points ([04f7b0d](https://github.com/rvben/upd/commit/04f7b0d6362f32ff2a6212386f310b481bfd7d56))
|
|
24
|
+
- **cli**: add --insecure global flag ([df1943f](https://github.com/rvben/upd/commit/df1943ff53369b0cc0b32401cbbedb1095f07230))
|
|
25
|
+
- **http**: attach TLS hint to send errors via wrap_send_err ([9b1fd2c](https://github.com/rvben/upd/commit/9b1fd2cef7e307a6d3328074a82353a772a5b44c))
|
|
26
|
+
- **http**: apply TLS options to ClientBuilder ([7d8f616](https://github.com/rvben/upd/commit/7d8f6161f5f7be31e829f89ba9aaeb332532229f))
|
|
27
|
+
- **http**: implement init() over pure helpers ([79556c6](https://github.com/rvben/upd/commit/79556c6d5a34cf5eda97e645b4d62c62ad706809))
|
|
28
|
+
- **http**: detect TLS trust failures in error chains ([e63be5d](https://github.com/rvben/upd/commit/e63be5d62cfa82f2a73f3e78e294c65868c53ba7))
|
|
29
|
+
- **http**: parse PEM CA bundle with multi-cert support ([07e3983](https://github.com/rvben/upd/commit/07e3983a9477dad042ea0f5b10ee8efa9382b384))
|
|
30
|
+
- **http**: resolve CA bundle path from env vars ([2bd42ec](https://github.com/rvben/upd/commit/2bd42ec7018094e1daf399af8fb2e7e288e9e206))
|
|
31
|
+
- **http**: scaffold TLS options module ([3f797cc](https://github.com/rvben/upd/commit/3f797cc658298b9b952e566b49d4998a4decfc64))
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **http**: propagate --insecure global flag to self-update ([88b397e](https://github.com/rvben/upd/commit/88b397ee4a169eab94b56247050e8b1814c8ccf3))
|
|
36
|
+
- **http**: defer TLS init past offline and no-op early returns ([41cfb40](https://github.com/rvben/upd/commit/41cfb40f2f72e6090fc0f083c13371c3a4269347))
|
|
37
|
+
- **http**: skip CA bundle resolution when --insecure is set ([8d73e27](https://github.com/rvben/upd/commit/8d73e27aadaa5ac488bbbf7f556476cf9bfaa0e6))
|
|
38
|
+
|
|
18
39
|
## [0.1.5](https://github.com/rvben/upd/compare/v0.1.4...v0.1.5) - 2026-04-28
|
|
19
40
|
|
|
20
41
|
### Added
|
|
@@ -203,10 +203,11 @@ impl OsvClient {
|
|
|
203
203
|
/// Create a client pointing at a custom base URL (used by tests).
|
|
204
204
|
pub fn with_base_url(base_url: String) -> Self {
|
|
205
205
|
Self {
|
|
206
|
-
client:
|
|
207
|
-
.timeout(Duration::from_secs(30))
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
client: crate::http::apply(
|
|
207
|
+
Client::builder().timeout(Duration::from_secs(30))
|
|
208
|
+
)
|
|
209
|
+
.build()
|
|
210
|
+
.expect("Failed to create HTTP client. This usually indicates a TLS/SSL configuration issue on your system."),
|
|
210
211
|
base_url,
|
|
211
212
|
}
|
|
212
213
|
}
|
|
@@ -333,12 +334,14 @@ impl OsvClient {
|
|
|
333
334
|
|
|
334
335
|
let request = OsvBatchRequest { queries };
|
|
335
336
|
|
|
337
|
+
let url = format!("{}/querybatch", self.base_url);
|
|
336
338
|
let mut response = self
|
|
337
339
|
.client
|
|
338
|
-
.post(
|
|
340
|
+
.post(&url)
|
|
339
341
|
.json(&request)
|
|
340
342
|
.send()
|
|
341
|
-
.await
|
|
343
|
+
.await
|
|
344
|
+
.map_err(|e| crate::http::wrap_send_err(e, &url))?;
|
|
342
345
|
|
|
343
346
|
if !response.status().is_success() {
|
|
344
347
|
let status = response.status();
|
|
@@ -397,11 +400,13 @@ impl OsvClient {
|
|
|
397
400
|
|
|
398
401
|
/// Fetch vulnerability details by ID
|
|
399
402
|
async fn fetch_vuln_by_id(&self, id: &str) -> Result<Vulnerability> {
|
|
403
|
+
let url = format!("{}/vulns/{}", self.base_url, id);
|
|
400
404
|
let response = self
|
|
401
405
|
.client
|
|
402
|
-
.get(
|
|
406
|
+
.get(&url)
|
|
403
407
|
.send()
|
|
404
|
-
.await
|
|
408
|
+
.await
|
|
409
|
+
.map_err(|e| crate::http::wrap_send_err(e, &url))?;
|
|
405
410
|
|
|
406
411
|
if !response.status().is_success() {
|
|
407
412
|
anyhow::bail!(
|
|
@@ -192,6 +192,16 @@ pub struct Cli {
|
|
|
192
192
|
/// Explicit file paths are always processed regardless of this flag.
|
|
193
193
|
#[arg(long = "no-ignore", global = true)]
|
|
194
194
|
pub no_ignore: bool,
|
|
195
|
+
|
|
196
|
+
/// Disable TLS certificate verification.
|
|
197
|
+
///
|
|
198
|
+
/// Skips verification of server certificates for all HTTPS requests this run.
|
|
199
|
+
/// Hostname verification is also disabled on a best-effort basis (may not be
|
|
200
|
+
/// honored under every TLS backend). Use only as a last resort in environments
|
|
201
|
+
/// where the system trust store cannot be configured. Prefer setting
|
|
202
|
+
/// REQUESTS_CA_BUNDLE or SSL_CERT_FILE to your corporate CA bundle.
|
|
203
|
+
#[arg(long, global = true)]
|
|
204
|
+
pub insecure: bool,
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
#[derive(Subcommand)]
|
|
@@ -297,6 +307,7 @@ mod tests {
|
|
|
297
307
|
assert!(cli.paths.is_empty());
|
|
298
308
|
assert!(cli.command.is_none());
|
|
299
309
|
assert!(cli.min_age.is_none());
|
|
310
|
+
assert!(!cli.insecure);
|
|
300
311
|
}
|
|
301
312
|
|
|
302
313
|
#[test]
|
|
@@ -874,4 +885,24 @@ mod tests {
|
|
|
874
885
|
assert!(cli.no_ignore);
|
|
875
886
|
assert!(matches!(cli.command, Some(Command::Align { .. })));
|
|
876
887
|
}
|
|
888
|
+
|
|
889
|
+
#[test]
|
|
890
|
+
fn test_cli_parses_insecure() {
|
|
891
|
+
let cli = Cli::try_parse_from(["upd", "--insecure"]).unwrap();
|
|
892
|
+
assert!(cli.insecure);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#[test]
|
|
896
|
+
fn test_cli_insecure_default_false() {
|
|
897
|
+
let cli = Cli::try_parse_from(["upd"]).unwrap();
|
|
898
|
+
assert!(!cli.insecure);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
#[test]
|
|
902
|
+
fn test_cli_insecure_is_global_across_subcommands() {
|
|
903
|
+
for sub in ["audit", "update", "align"] {
|
|
904
|
+
let cli = Cli::try_parse_from(["upd", sub, "--insecure"]).unwrap();
|
|
905
|
+
assert!(cli.insecure, "--insecure must be accepted under `{sub}`");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
877
908
|
}
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
//! TLS configuration for HTTP clients.
|
|
2
|
+
//!
|
|
3
|
+
//! This module owns a process-global `HttpOptions` (initialized once per networked
|
|
4
|
+
//! subcommand) describing extra CA certificates and an `--insecure` flag. Each
|
|
5
|
+
//! `Client::builder()` chain in the codebase calls [`apply`] to inherit those options.
|
|
6
|
+
//!
|
|
7
|
+
//! Pure helpers ([`resolve_ca_path`], [`parse_pem_bundle`], [`chain_indicates_tls_failure`])
|
|
8
|
+
//! contain the testable logic; [`init`] is a thin shell over them.
|
|
9
|
+
|
|
10
|
+
use anyhow::{Context, Result};
|
|
11
|
+
use reqwest::{Certificate, ClientBuilder};
|
|
12
|
+
use std::path::{Path, PathBuf};
|
|
13
|
+
use std::sync::OnceLock;
|
|
14
|
+
|
|
15
|
+
const CA_BUNDLE_ENV_VARS: &[&str] = &[
|
|
16
|
+
"UPD_CA_BUNDLE",
|
|
17
|
+
"REQUESTS_CA_BUNDLE",
|
|
18
|
+
"CURL_CA_BUNDLE",
|
|
19
|
+
"SSL_CERT_FILE",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
#[derive(Debug, Default)]
|
|
23
|
+
pub struct HttpOptions {
|
|
24
|
+
pub insecure: bool,
|
|
25
|
+
/// PEM-parsed certificates loaded once at startup from env-var-resolved paths.
|
|
26
|
+
pub extra_certs: Vec<Certificate>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static HTTP_OPTIONS: OnceLock<HttpOptions> = OnceLock::new();
|
|
30
|
+
static DEFAULT_OPTIONS: OnceLock<HttpOptions> = OnceLock::new();
|
|
31
|
+
|
|
32
|
+
/// Resolve which CA-bundle env var holds the path we should load.
|
|
33
|
+
///
|
|
34
|
+
/// Returns the first non-empty value among `UPD_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`,
|
|
35
|
+
/// `CURL_CA_BUNDLE`, `SSL_CERT_FILE` (in that order), or `None` when none are set.
|
|
36
|
+
/// Empty strings are treated as unset.
|
|
37
|
+
pub(crate) fn resolve_ca_path<E>(env: E) -> Option<PathBuf>
|
|
38
|
+
where
|
|
39
|
+
E: Fn(&str) -> Option<String>,
|
|
40
|
+
{
|
|
41
|
+
CA_BUNDLE_ENV_VARS
|
|
42
|
+
.iter()
|
|
43
|
+
.filter_map(|name| env(name))
|
|
44
|
+
.find(|v| !v.is_empty())
|
|
45
|
+
.map(PathBuf::from)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Parse one or more PEM-encoded certificates from a byte buffer.
|
|
49
|
+
///
|
|
50
|
+
/// Treats `from_pem_bundle` failures as fatal. An empty buffer (or a buffer with
|
|
51
|
+
/// no `BEGIN CERTIFICATE` markers) returns an `Err` so callers can surface a
|
|
52
|
+
/// meaningful diagnostic — silently producing an empty cert list would mask a
|
|
53
|
+
/// misconfigured CA bundle path.
|
|
54
|
+
pub(crate) fn parse_pem_bundle(bytes: &[u8]) -> Result<Vec<Certificate>> {
|
|
55
|
+
if bytes.is_empty() {
|
|
56
|
+
anyhow::bail!("CA bundle is empty (no PEM data)");
|
|
57
|
+
}
|
|
58
|
+
let certs =
|
|
59
|
+
Certificate::from_pem_bundle(bytes).context("failed to parse PEM certificate bundle")?;
|
|
60
|
+
if certs.is_empty() {
|
|
61
|
+
anyhow::bail!("no certificates found in PEM bundle");
|
|
62
|
+
}
|
|
63
|
+
Ok(certs)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TLS_MARKERS: &[&str] = &[
|
|
67
|
+
"unknown issuer",
|
|
68
|
+
"unknownissuer",
|
|
69
|
+
"invalidcertificate",
|
|
70
|
+
"self-signed",
|
|
71
|
+
"self signed",
|
|
72
|
+
"certificate verify",
|
|
73
|
+
"certificateverification",
|
|
74
|
+
"tls handshake",
|
|
75
|
+
"certnotvalidforname",
|
|
76
|
+
"certificate not valid for name",
|
|
77
|
+
"certexpired",
|
|
78
|
+
"cert expired",
|
|
79
|
+
"certnotvalidyet",
|
|
80
|
+
"certrevoked",
|
|
81
|
+
"unknownrevocationstatus",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/// Walk an error chain and return true if any source's `Display` contains a
|
|
85
|
+
/// known TLS-trust-failure marker (case-insensitive).
|
|
86
|
+
///
|
|
87
|
+
/// Generic over `dyn std::error::Error` so it can be unit-tested with synthetic
|
|
88
|
+
/// error types — `reqwest::Error` has no public constructor for arbitrary chains.
|
|
89
|
+
pub(crate) fn chain_indicates_tls_failure(err: &(dyn std::error::Error + 'static)) -> bool {
|
|
90
|
+
let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
|
|
91
|
+
while let Some(e) = current {
|
|
92
|
+
let msg = e.to_string().to_lowercase();
|
|
93
|
+
if TLS_MARKERS.iter().any(|m| msg.contains(m)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
current = e.source();
|
|
97
|
+
}
|
|
98
|
+
false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Compute the extra-certs vector that `init` would set, given injectable env
|
|
102
|
+
/// and file-read closures. Pulled out so the short-circuit and error paths can
|
|
103
|
+
/// be exercised hermetically without touching the process-global `OnceLock`.
|
|
104
|
+
fn compute_extra_certs<E, R>(insecure: bool, env: E, read: R) -> Result<Vec<Certificate>>
|
|
105
|
+
where
|
|
106
|
+
E: Fn(&str) -> Option<String>,
|
|
107
|
+
R: Fn(&Path) -> std::io::Result<Vec<u8>>,
|
|
108
|
+
{
|
|
109
|
+
if insecure {
|
|
110
|
+
return Ok(Vec::new());
|
|
111
|
+
}
|
|
112
|
+
let Some(p) = resolve_ca_path(env) else {
|
|
113
|
+
return Ok(Vec::new());
|
|
114
|
+
};
|
|
115
|
+
let bytes = read(&p).with_context(|| format!("failed to read CA bundle at {}", p.display()))?;
|
|
116
|
+
parse_pem_bundle(&bytes)
|
|
117
|
+
.with_context(|| format!("failed to parse CA bundle at {}", p.display()))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Initialize TLS options. Called from the entry point of every networked
|
|
121
|
+
/// subcommand (`run_update`, `run_align`, `run_audit`, `self_update`) before
|
|
122
|
+
/// any [`reqwest::Client`] is built.
|
|
123
|
+
///
|
|
124
|
+
/// Reads CA paths from `UPD_CA_BUNDLE` → `REQUESTS_CA_BUNDLE` → `CURL_CA_BUNDLE` →
|
|
125
|
+
/// `SSL_CERT_FILE` (priority order; first non-empty wins). Returns `Err` with the
|
|
126
|
+
/// path in the message if the chosen file cannot be read or parsed.
|
|
127
|
+
///
|
|
128
|
+
/// When `insecure` is true, env-var-resolved bundles are not read or parsed: the
|
|
129
|
+
/// user has opted out of verification entirely, so a stale or malformed bundle
|
|
130
|
+
/// path must not block the run.
|
|
131
|
+
///
|
|
132
|
+
/// **Library callers:** clients constructed before `init` is called see
|
|
133
|
+
/// [`HttpOptions::default`] forever — only clients constructed after `init`
|
|
134
|
+
/// pick up the configured CAs and `--insecure` flag.
|
|
135
|
+
pub fn init(insecure: bool) -> Result<()> {
|
|
136
|
+
let extra_certs =
|
|
137
|
+
compute_extra_certs(insecure, |k| std::env::var(k).ok(), |p| std::fs::read(p))?;
|
|
138
|
+
// OnceLock::set is fallible if already set; that's fine — first init wins, later
|
|
139
|
+
// calls in the same process are silent no-ops (the value is already correct).
|
|
140
|
+
let _ = HTTP_OPTIONS.set(HttpOptions {
|
|
141
|
+
insecure,
|
|
142
|
+
extra_certs,
|
|
143
|
+
});
|
|
144
|
+
Ok(())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Returns the initialized options, or [`HttpOptions::default`] if `init` was
|
|
148
|
+
/// never called (the contract used by library tests).
|
|
149
|
+
pub fn options() -> &'static HttpOptions {
|
|
150
|
+
HTTP_OPTIONS
|
|
151
|
+
.get()
|
|
152
|
+
.unwrap_or_else(|| DEFAULT_OPTIONS.get_or_init(HttpOptions::default))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Apply the configured TLS options to a [`ClientBuilder`].
|
|
156
|
+
pub fn apply(mut builder: ClientBuilder) -> ClientBuilder {
|
|
157
|
+
let opts = options();
|
|
158
|
+
for cert in &opts.extra_certs {
|
|
159
|
+
builder = builder.add_root_certificate(cert.clone());
|
|
160
|
+
}
|
|
161
|
+
if opts.insecure {
|
|
162
|
+
builder = builder
|
|
163
|
+
.danger_accept_invalid_certs(true)
|
|
164
|
+
.danger_accept_invalid_hostnames(true);
|
|
165
|
+
}
|
|
166
|
+
builder
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Build the user-facing TLS hint for a given URL.
|
|
170
|
+
fn tls_hint(url: &str) -> String {
|
|
171
|
+
let host = url::Url::parse(url)
|
|
172
|
+
.ok()
|
|
173
|
+
.and_then(|u| u.host_str().map(str::to_owned))
|
|
174
|
+
.unwrap_or_else(|| url.to_string());
|
|
175
|
+
format!(
|
|
176
|
+
"TLS certificate verification failed for {host}. \
|
|
177
|
+
If you're behind a corporate proxy, install your CA into the system trust store \
|
|
178
|
+
or set REQUESTS_CA_BUNDLE / SSL_CERT_FILE to your CA bundle path. \
|
|
179
|
+
As a last resort, pass --insecure to skip verification (not recommended)."
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Map a [`reqwest::Error`] from `.send()` into an [`anyhow::Error`], attaching
|
|
184
|
+
/// a TLS-trust hint when the error chain indicates a certificate-verification
|
|
185
|
+
/// failure.
|
|
186
|
+
pub fn wrap_send_err(err: reqwest::Error, url: &str) -> anyhow::Error {
|
|
187
|
+
if chain_indicates_tls_failure(&err) {
|
|
188
|
+
let hint = tls_hint(url);
|
|
189
|
+
anyhow::Error::from(err).context(hint)
|
|
190
|
+
} else {
|
|
191
|
+
anyhow::Error::from(err)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[cfg(test)]
|
|
196
|
+
mod tests {
|
|
197
|
+
use super::*;
|
|
198
|
+
|
|
199
|
+
fn fake_env<'a>(map: &'a [(&'a str, Option<&'a str>)]) -> impl Fn(&str) -> Option<String> + 'a {
|
|
200
|
+
move |k: &str| {
|
|
201
|
+
map.iter()
|
|
202
|
+
.find(|(name, _)| *name == k)
|
|
203
|
+
.and_then(|(_, v)| v.map(str::to_owned))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn test_resolve_ca_path_priority_order() {
|
|
209
|
+
// UPD_CA_BUNDLE wins
|
|
210
|
+
let env = fake_env(&[
|
|
211
|
+
("UPD_CA_BUNDLE", Some("/upd")),
|
|
212
|
+
("REQUESTS_CA_BUNDLE", Some("/requests")),
|
|
213
|
+
("CURL_CA_BUNDLE", Some("/curl")),
|
|
214
|
+
("SSL_CERT_FILE", Some("/ssl")),
|
|
215
|
+
]);
|
|
216
|
+
assert_eq!(resolve_ca_path(&env), Some(PathBuf::from("/upd")));
|
|
217
|
+
|
|
218
|
+
// Without UPD_CA_BUNDLE, REQUESTS_CA_BUNDLE wins
|
|
219
|
+
let env = fake_env(&[
|
|
220
|
+
("UPD_CA_BUNDLE", None),
|
|
221
|
+
("REQUESTS_CA_BUNDLE", Some("/requests")),
|
|
222
|
+
("CURL_CA_BUNDLE", Some("/curl")),
|
|
223
|
+
("SSL_CERT_FILE", Some("/ssl")),
|
|
224
|
+
]);
|
|
225
|
+
assert_eq!(resolve_ca_path(&env), Some(PathBuf::from("/requests")));
|
|
226
|
+
|
|
227
|
+
// Down to SSL_CERT_FILE
|
|
228
|
+
let env = fake_env(&[("SSL_CERT_FILE", Some("/ssl"))]);
|
|
229
|
+
assert_eq!(resolve_ca_path(&env), Some(PathBuf::from("/ssl")));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn test_resolve_ca_path_skips_empty_strings() {
|
|
234
|
+
let env = fake_env(&[
|
|
235
|
+
("UPD_CA_BUNDLE", Some("")),
|
|
236
|
+
("REQUESTS_CA_BUNDLE", Some("/requests")),
|
|
237
|
+
]);
|
|
238
|
+
assert_eq!(resolve_ca_path(&env), Some(PathBuf::from("/requests")));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn test_resolve_ca_path_returns_none_when_all_unset() {
|
|
243
|
+
let env = fake_env(&[]);
|
|
244
|
+
assert_eq!(resolve_ca_path(&env), None);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const TEST_CERT_1: &[u8] = br#"-----BEGIN CERTIFICATE-----
|
|
248
|
+
MIIDCzCCAfOgAwIBAgIUKZ0KJZK5GE4JM+j0245augoh8OwwDQYJKoZIhvcNAQEL
|
|
249
|
+
BQAwFTETMBEGA1UEAwwKdXBkLXRlc3QtMTAeFw0yNjA0MjkwODI5MjhaFw0zNjA0
|
|
250
|
+
MjYwODI5MjhaMBUxEzARBgNVBAMMCnVwZC10ZXN0LTEwggEiMA0GCSqGSIb3DQEB
|
|
251
|
+
AQUAA4IBDwAwggEKAoIBAQDc44mX7cV5VLqtFGPGnEjjRLC+vO6Tk2SSO3eC9S3M
|
|
252
|
+
6Ryy3sAl11ZeSLzBOhdE3rZC4+oZjiy7Ht8TJuz8XxMH4ASUrfdkO7CyJfbxE5FJ
|
|
253
|
+
FAb+ZbHE9K+SKiCiVPd367v05Tra2P31+k3qFYnqH9jklwj1RwdMNhNLNUDd0f7I
|
|
254
|
+
xiTEGnSEfNUy1opwAcwYdRIAYAWi9TQxLy6+JyVHaDSG9s05SbinKaRWb/5Elopc
|
|
255
|
+
QJZ0fa8caCW7t+6caQQNE4pbgwj9iLSstyo28MAiZcQ7vgOzxCvyxf/fhQbbuCjg
|
|
256
|
+
uCUlTb9QsToXjF6GrQ6ZY5ze0KslQIa1KHfEtt8C9PnlAgMBAAGjUzBRMB0GA1Ud
|
|
257
|
+
DgQWBBSMYjXeEDAT+plKMitN3kuxrtU+eTAfBgNVHSMEGDAWgBSMYjXeEDAT+plK
|
|
258
|
+
MitN3kuxrtU+eTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB3
|
|
259
|
+
k413BI7F3FEOsjaCaSqr+Cp+xaPytGFxwjB7y7lXk3Ep0sCwSO99QDze+hXAWY+L
|
|
260
|
+
32JHgNXjvKccgfb+VP8Cv7XR5vNBmOm8RXfE90r3YwgPQo2GfAK3KAclFabil9ek
|
|
261
|
+
Dhjy52Y3Kw3S1ZfjizJbKlT0WvLLIaFVAHxOKCxwTNnYzu9j4rF2krE74yTdaPBm
|
|
262
|
+
bOIOTW/JL8Xlux+k0jV+BvLYK8/rlKgjUlCUlQFwKfQVVTagnh66jQbvtLqGT/1z
|
|
263
|
+
5AQ1MkjwT9arAOD+xWKXd3F1e2uEFl8QTGSqCTv85gKFM2BAVS04295SUUfllbNd
|
|
264
|
+
wVzHoiFTA+cbUjjOxjGM
|
|
265
|
+
-----END CERTIFICATE-----
|
|
266
|
+
"#;
|
|
267
|
+
const TEST_CERT_2: &[u8] = br#"-----BEGIN CERTIFICATE-----
|
|
268
|
+
MIIDCzCCAfOgAwIBAgIUTRxLSgg2B1Nv53xT2+s2LrPEze4wDQYJKoZIhvcNAQEL
|
|
269
|
+
BQAwFTETMBEGA1UEAwwKdXBkLXRlc3QtMjAeFw0yNjA0MjkwODI5MjhaFw0zNjA0
|
|
270
|
+
MjYwODI5MjhaMBUxEzARBgNVBAMMCnVwZC10ZXN0LTIwggEiMA0GCSqGSIb3DQEB
|
|
271
|
+
AQUAA4IBDwAwggEKAoIBAQDaP51Ef0k52LrqC9PZ1kW/XlSqNXuTLgHeOPFPJmxQ
|
|
272
|
+
lVQ9/3dRAE3gnJUZmOsoZ5lgceNhBuPArrHJytDMUFJ5auRZhF1i7LOSRYF9B7KW
|
|
273
|
+
gQmlniAI9+rYBMTgc/2+PnnB3dGHcg23sVwtYWzTW6DZl6z28cpUXgLES7sSPi+0
|
|
274
|
+
hleTvfsEW2idjFcZO6sOFEeyPA3PJUA0YtWdKLANRp5kIEEqQO5Bln8MOEbl6r2O
|
|
275
|
+
vJOZnUTCoqP1Y2xExAj2gUw/+CWzAjC0rp4uPKUWP0ckHGlbCm7KmqxYLE6o0obg
|
|
276
|
+
c9vXxyeZ0FbZLc60e/mB3iakxJWa8DK1T9DH7gt5cXBfAgMBAAGjUzBRMB0GA1Ud
|
|
277
|
+
DgQWBBQLAmfP5m+XU+67B7/u6l89s6p7wTAfBgNVHSMEGDAWgBQLAmfP5m+XU+67
|
|
278
|
+
B7/u6l89s6p7wTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBW
|
|
279
|
+
L0eKTSQx7KJ4F5N7WDgdZvp3QtonBWIgQzRxgWEpS8oQyS0Jtr5esOmCSMbPnPNj
|
|
280
|
+
2xSE001niyxlsgjd4/UQ3P9Kj8jHfElnNB/qjSGPWzTRb/T+2LLXWQZ3ptcN8p4O
|
|
281
|
+
JeCGZf6GyCJEu9ToiPddNZl0B9IELCUSBJ8QPbIVBRHCIbCDIe5bIt3gJKBnK+Vl
|
|
282
|
+
8eUKTvF8cnnvw2Nr0umbxCoVjbmbyKpL/3ZsT70QF4eyQZ0JAmYQg7Ufx+pj09FX
|
|
283
|
+
j6KbS4KlY8roCKAlQEAxJ1qXTvl2/6QcWHvux1nue0KcBfHvFBAIHfNVUj7NslXX
|
|
284
|
+
uMhJbUlN9AYtL2pAGNPK
|
|
285
|
+
-----END CERTIFICATE-----
|
|
286
|
+
"#;
|
|
287
|
+
|
|
288
|
+
fn two_cert_bundle() -> Vec<u8> {
|
|
289
|
+
let mut out = Vec::with_capacity(TEST_CERT_1.len() + TEST_CERT_2.len());
|
|
290
|
+
out.extend_from_slice(TEST_CERT_1);
|
|
291
|
+
out.extend_from_slice(TEST_CERT_2);
|
|
292
|
+
out
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn test_parse_pem_bundle_loads_multiple_certs() {
|
|
297
|
+
let bytes = two_cert_bundle();
|
|
298
|
+
let certs = parse_pem_bundle(&bytes).expect("two-cert bundle must parse");
|
|
299
|
+
assert_eq!(certs.len(), 2, "expected 2 certs, got {}", certs.len());
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn test_parse_pem_bundle_rejects_garbage() {
|
|
304
|
+
let bytes = b"this is not a PEM file at all";
|
|
305
|
+
let err = parse_pem_bundle(bytes).expect_err("garbage must not parse");
|
|
306
|
+
let msg = err.to_string().to_lowercase();
|
|
307
|
+
assert!(
|
|
308
|
+
msg.contains("pem") || msg.contains("certificate"),
|
|
309
|
+
"expected PEM-related error, got: {msg}"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn test_parse_pem_bundle_handles_empty_input() {
|
|
315
|
+
let err = parse_pem_bundle(&[]).expect_err("empty input must not parse");
|
|
316
|
+
let msg = err.to_string().to_lowercase();
|
|
317
|
+
assert!(
|
|
318
|
+
msg.contains("empty") || msg.contains("no certificate") || msg.contains("pem"),
|
|
319
|
+
"expected empty/no-cert error, got: {msg}"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
use std::error::Error as StdError;
|
|
324
|
+
use std::fmt;
|
|
325
|
+
|
|
326
|
+
#[derive(Debug)]
|
|
327
|
+
struct TestError {
|
|
328
|
+
msg: String,
|
|
329
|
+
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
impl TestError {
|
|
333
|
+
fn new(msg: &str) -> Self {
|
|
334
|
+
Self {
|
|
335
|
+
msg: msg.to_string(),
|
|
336
|
+
source: None,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
fn with_source(mut self, src: Box<dyn StdError + Send + Sync + 'static>) -> Self {
|
|
340
|
+
self.source = Some(src);
|
|
341
|
+
self
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
impl fmt::Display for TestError {
|
|
346
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
347
|
+
f.write_str(&self.msg)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
impl StdError for TestError {
|
|
352
|
+
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
|
353
|
+
self.source
|
|
354
|
+
.as_deref()
|
|
355
|
+
.map(|s| s as &(dyn StdError + 'static))
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
fn test_chain_indicates_tls_failure_for_each_marker() {
|
|
361
|
+
let markers = [
|
|
362
|
+
"UnknownIssuer",
|
|
363
|
+
"unknown issuer",
|
|
364
|
+
"InvalidCertificate",
|
|
365
|
+
"invalidcertificate",
|
|
366
|
+
"self-signed",
|
|
367
|
+
"self signed",
|
|
368
|
+
"certificate verify failed",
|
|
369
|
+
"CertificateVerification",
|
|
370
|
+
"tls handshake failure",
|
|
371
|
+
"CertNotValidForName",
|
|
372
|
+
"certificate not valid for name",
|
|
373
|
+
"CertExpired",
|
|
374
|
+
"cert expired",
|
|
375
|
+
"CertNotValidYet",
|
|
376
|
+
"CertRevoked",
|
|
377
|
+
"UnknownRevocationStatus",
|
|
378
|
+
];
|
|
379
|
+
for m in markers {
|
|
380
|
+
let e = TestError::new(m);
|
|
381
|
+
assert!(
|
|
382
|
+
chain_indicates_tls_failure(&e),
|
|
383
|
+
"expected marker {m:?} to indicate TLS failure"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#[test]
|
|
389
|
+
fn test_chain_indicates_tls_failure_passthrough() {
|
|
390
|
+
for msg in [
|
|
391
|
+
"connection refused",
|
|
392
|
+
"dns lookup failed",
|
|
393
|
+
"timed out",
|
|
394
|
+
"operation cancelled",
|
|
395
|
+
"broken pipe",
|
|
396
|
+
] {
|
|
397
|
+
let e = TestError::new(msg);
|
|
398
|
+
assert!(
|
|
399
|
+
!chain_indicates_tls_failure(&e),
|
|
400
|
+
"non-TLS message {msg:?} must not match"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#[test]
|
|
406
|
+
fn test_chain_indicates_tls_failure_walks_sources() {
|
|
407
|
+
let inner = TestError::new("InvalidCertificate(UnknownIssuer)");
|
|
408
|
+
let outer = TestError::new("error sending request").with_source(Box::new(inner));
|
|
409
|
+
assert!(
|
|
410
|
+
chain_indicates_tls_failure(&outer),
|
|
411
|
+
"matcher must walk Error::source"
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
use serial_test::serial;
|
|
416
|
+
|
|
417
|
+
#[test]
|
|
418
|
+
#[serial]
|
|
419
|
+
fn test_init_smoke_default() {
|
|
420
|
+
// Note: `init` only sets the OnceLock the FIRST time across the test process.
|
|
421
|
+
// This test asserts the post-init state is sane regardless of whether it ran
|
|
422
|
+
// first; we don't assert the exact value of `insecure` since another #[serial]
|
|
423
|
+
// test in this process may have already initialized it.
|
|
424
|
+
let _ = init(false);
|
|
425
|
+
let opts = options();
|
|
426
|
+
let _ = opts.insecure;
|
|
427
|
+
let _ = &opts.extra_certs;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
#[serial]
|
|
432
|
+
fn test_options_default_when_uninitialized() {
|
|
433
|
+
// This test only meaningfully runs in a process where init() was never called.
|
|
434
|
+
// Under nextest's default per-test process model this is hermetic. Under
|
|
435
|
+
// `cargo test`'s shared process, we tolerate either default or initialized state.
|
|
436
|
+
let opts = options();
|
|
437
|
+
let _ = opts.insecure;
|
|
438
|
+
let _ = &opts.extra_certs;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[test]
|
|
442
|
+
fn test_init_bad_path_via_helpers() {
|
|
443
|
+
// We can't safely invoke `init()` with a custom env in the test process
|
|
444
|
+
// because of the OnceLock. Instead, exercise the underlying composition:
|
|
445
|
+
// resolve → read → parse, which is exactly what `init` does internally.
|
|
446
|
+
let path = PathBuf::from("/this/path/definitely/does/not/exist/upd-ca.pem");
|
|
447
|
+
let read_err = std::fs::read(&path).expect_err("read of missing path must fail");
|
|
448
|
+
assert_eq!(read_err.kind(), std::io::ErrorKind::NotFound);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#[test]
|
|
452
|
+
fn test_compute_extra_certs_insecure_short_circuits() {
|
|
453
|
+
// With insecure=true, env vars and file reads must not be consulted at all.
|
|
454
|
+
// We assert this by passing closures that would panic if called.
|
|
455
|
+
let env =
|
|
456
|
+
|_: &str| -> Option<String> { panic!("env should not be read when insecure=true") };
|
|
457
|
+
let read = |_: &Path| -> std::io::Result<Vec<u8>> {
|
|
458
|
+
panic!("read should not be called when insecure=true")
|
|
459
|
+
};
|
|
460
|
+
let certs = compute_extra_certs(true, env, read).expect("insecure must succeed");
|
|
461
|
+
assert!(certs.is_empty(), "insecure mode must yield no extra certs");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
#[test]
|
|
465
|
+
fn test_compute_extra_certs_insecure_ignores_broken_bundle() {
|
|
466
|
+
// Even with a stale env var pointing at a bogus path, insecure=true must succeed.
|
|
467
|
+
let env = fake_env(&[("UPD_CA_BUNDLE", Some("/nope/missing.pem"))]);
|
|
468
|
+
let read = |_: &Path| -> std::io::Result<Vec<u8>> {
|
|
469
|
+
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
|
|
470
|
+
};
|
|
471
|
+
let certs = compute_extra_certs(true, env, read).expect("insecure must ignore bad bundle");
|
|
472
|
+
assert!(certs.is_empty());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#[test]
|
|
476
|
+
fn test_compute_extra_certs_secure_propagates_read_error_with_path() {
|
|
477
|
+
// With insecure=false, a read error must surface and include the resolved path.
|
|
478
|
+
let env = fake_env(&[("UPD_CA_BUNDLE", Some("/nope/missing.pem"))]);
|
|
479
|
+
let read = |_: &Path| -> std::io::Result<Vec<u8>> {
|
|
480
|
+
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
|
|
481
|
+
};
|
|
482
|
+
let err = compute_extra_certs(false, env, read).expect_err("missing path must error");
|
|
483
|
+
let chain = format!("{err:#}");
|
|
484
|
+
assert!(
|
|
485
|
+
chain.contains("/nope/missing.pem"),
|
|
486
|
+
"error should mention path, got: {chain}"
|
|
487
|
+
);
|
|
488
|
+
assert!(
|
|
489
|
+
chain.to_lowercase().contains("failed to read"),
|
|
490
|
+
"error should describe read failure, got: {chain}"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
#[test]
|
|
495
|
+
fn test_compute_extra_certs_no_env_returns_empty() {
|
|
496
|
+
let env = fake_env(&[]);
|
|
497
|
+
let read = |_: &Path| -> std::io::Result<Vec<u8>> {
|
|
498
|
+
panic!("read should not be called when no env var is set")
|
|
499
|
+
};
|
|
500
|
+
let certs = compute_extra_certs(false, env, read).expect("no env must succeed");
|
|
501
|
+
assert!(certs.is_empty());
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#[test]
|
|
505
|
+
fn test_apply_with_default_options_builds() {
|
|
506
|
+
// Contract test: we can't introspect a ClientBuilder's TLS state, so the
|
|
507
|
+
// best we can do is assert that the chain compiles and produces a usable
|
|
508
|
+
// Client. With default options (no extra certs, not insecure), apply
|
|
509
|
+
// must be a no-op that doesn't break the builder.
|
|
510
|
+
let client = apply(reqwest::Client::builder()).build();
|
|
511
|
+
assert!(
|
|
512
|
+
client.is_ok(),
|
|
513
|
+
"apply(builder).build() must succeed: {client:?}"
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|