use std::collections::BTreeMap;
use pretty_assertions::assert_eq;
use super::*;
fn sources(user: &str, project: Option<&str>) -> OpenApiSources {
OpenApiSources {
user: toml::from_str(user).unwrap(),
user_dir: PathBuf::from("/home/me/.rho"),
project: project.map(|text| ProjectSources {
trust: ProjectTrust::Trusted,
path: PathBuf::from("/repo/.rho/openapi.toml"),
root: PathBuf::from("/repo"),
config: parse_project_file(text),
}),
}
}
fn env(name: &str) -> Option<String> {
BTreeMap::from([("TOKEN", "t0ken"), ("USER", "me"), ("PASS", "pw")])
.get(name)
.map(|value| (*value).to_owned())
}
fn outcome(plans: &SourcePlans) -> (Vec<&str>, Vec<SourceFailure>, Vec<String>) {
(
plans
.sources
.iter()
.map(|plan| plan.name.as_str())
.collect(),
plans.failures.clone(),
plans.warnings.clone(),
)
}
fn failure(name: &str, error: &str) -> SourceFailure {
SourceFailure {
name: Some(name.to_owned()),
error: error.to_owned(),
}
}
#[test]
fn project_sources_cannot_carry_credentials() {
let user = r#"
[github]
spec = "https://example.com/github.json"
base_url = "https://api.github.com"
auth = { type = "bearer", token_env = "TOKEN" }
"#;
let project = r#"
[openapi.github]
spec = "https://evil.example.com/github.json"
[openapi.petstore]
spec = "specs/petstore.yaml"
[openapi.leaky]
spec = "https://example.com/leaky.json"
base_url = "https://api.example.com"
auth = { type = "bearer", token_env = "TOKEN" }
headers_from_env = { x-key = "TOKEN" }
"#;
let plans = sources(user, Some(project)).plan(&env);
assert_eq!(
outcome(&plans),
(
vec!["github", "petstore"],
vec![failure(
"leaky",
"/repo/.rho/openapi.toml: source `leaky` sets `auth` and `headers_from_env` in project config; \
credentials come only from user config, so declare `[openapi.leaky]` in ~/.rho/config.toml instead",
)],
vec![
"OpenAPI source `github` in /repo/.rho/openapi.toml is ignored: user config declares the same source"
.to_owned()
],
)
);
assert_eq!(
(&plans.sources[0].spec, &plans.sources[1].spec),
(
&SpecLocation::Url(Url::parse("https://example.com/github.json").unwrap()),
&SpecLocation::Path(PathBuf::from("/repo").join("specs/petstore.yaml")),
)
);
}
#[test]
fn untrusted_projects_load_no_sources() {
let user = r#"
[github]
spec = "https://example.com/github.json"
"#;
let project = r#"
[openapi.petstore]
spec = "specs/petstore.yaml"
"#;
let mut untrusted = sources(user, Some(project));
if let Some(project) = untrusted.project.as_mut() {
project.trust = ProjectTrust::Untrusted;
}
assert_eq!(
outcome(&untrusted.plan(&env)),
(
vec!["github"],
Vec::new(),
vec![
"ignoring /repo/.rho/openapi.toml because this workspace is not trusted; \
set RHO_TRUST_PROJECT_OPENAPI=1 to load it"
.to_owned()
],
)
);
}
#[test]
fn invalid_sources_fail_alone() {
let cases = [
(
r#"auth = { type = "bearer", token_env = "TOKEN" }"#,
"source `api` sends credentials or headers, so it needs a `base_url`: they go only to that origin",
),
(
r#"headers = { authorization = "Bearer inline" }"#,
"source `api` sends credentials or headers, so it needs a `base_url`: they go only to that origin",
),
(
r#"base_url = "http://api.example.com"
headers_from_env = { x-key = "TOKEN" }"#,
"source `api` base_url, which carries credentials, must use HTTPS unless its host is loopback",
),
(
r#"base_url = "https://api.example.com"
auth = { type = "bearer", token_env = "MISSING" }"#,
"source `api` auth.token_env names environment variable `MISSING`, which is not set",
),
("timeout_secs = 0", "source `api` timeout_secs must be at least 1"),
(
r#"base_url = "https://api.example.com/?v=1""#,
"source `api` base_url `https://api.example.com/?v=1` must not have a query or fragment",
),
];
for (table, error) in cases {
let user = format!(
"[api]\nspec = \"https://example.com/api.json\"\n{table}\n\n[ok]\nspec = \"api.json\""
);
assert_eq!(
outcome(&sources(&user, None).plan(&env)),
(vec!["ok"], vec![failure("api", error)], Vec::new()),
"{table}"
);
}
}
#[test]
fn credentials_resolve_from_the_environment() {
let cases = [
(
r#"auth = { type = "bearer", token_env = "TOKEN" }"#,
vec![("authorization", "Bearer t0ken")],
None,
),
(
r#"auth = { type = "basic", username_env = "USER", password_env = "PASS" }"#,
vec![("authorization", "Basic bWU6cHc=")],
None,
),
(
r#"auth = { type = "api_key", in = "header", name = "X-Api-Key", value_env = "TOKEN" }
headers = { x-api-key = "literal", accept = "application/json" }"#,
vec![("accept", "application/json"), ("x-api-key", "t0ken")],
None,
),
(
r#"auth = { type = "api_key", in = "query", name = "key", value_env = "TOKEN" }"#,
vec![],
Some(("key".to_owned(), "t0ken".to_owned())),
),
];
for (table, headers, query) in cases {
let user =
format!("[api]\nspec = \"api.json\"\nbase_url = \"https://api.example.com\"\n{table}");
let plans = sources(&user, None).plan(&env);
assert_eq!(plans.failures, Vec::new(), "{table}");
let resolved = &plans.sources[0].headers;
let mut seen: Vec<(&str, &str)> = resolved
.headers
.iter()
.map(|(name, value)| (name.as_str(), value.to_str().unwrap()))
.collect();
seen.sort_unstable();
assert_eq!((seen, resolved.query.clone()), (headers, query), "{table}");
assert!(
resolved.headers.values().all(HeaderValue::is_sensitive),
"{table}"
);
}
}
#[test]
fn openapi_tables_round_trip_through_save() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[openapi.github]
spec = "https://example.com/github.json"
base_url = "https://api.github.com"
headers = { x-github-api-version = "2022-11-28" }
auth = { type = "api_key", in = "query", name = "key", value_env = "TOKEN" }
deny = ["GET /orgs/*"]
timeout_secs = 30
"#,
)
.unwrap();
let store = rho_providers::credentials::MemoryCredentialStore::default();
let config = crate::config::Config::load_with_store(path.clone(), &store).unwrap();
config.save_with_store(path.clone(), &store).unwrap();
let reloaded = crate::config::Config::load_with_store(path, &store).unwrap();
let expected = OpenApiSourceConfig {
spec: "https://example.com/github.json".into(),
base_url: Some("https://api.github.com".into()),
headers: BTreeMap::from([("x-github-api-version".into(), "2022-11-28".into())]),
headers_from_env: BTreeMap::new(),
auth: Some(AuthConfig::ApiKey {
value_env: "TOKEN".into(),
location: ApiKeyLocation::Query,
name: "key".into(),
}),
allow: Vec::new(),
deny: vec!["GET /orgs/*".into()],
timeout_secs: Some(30),
};
assert_eq!(
reloaded.openapi,
OpenApiConfig {
sources: BTreeMap::from([("github".into(), expected)]),
}
);
}