use std::sync::Arc;
mod common;
use bugwarden::config::Cli;
use bugwarden::server::{BugWarden, USER_AGENT};
use bugwarden_core::client::BugzillaClient;
use bugwarden_core::guard::Guard;
use bugwarden_core::policy::Policy;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[path = "common/pinned_cli.rs"]
mod pinned_cli;
#[path = "common/refused.rs"]
mod refused;
use pinned_cli::pinned;
#[test]
fn the_environment_pin_holds() {
pinned_cli::assert_the_pin_drops_every_fallback::<Cli>();
pinned_cli::assert_the_pin_neutralises_a_flag_added_later::<Cli>();
}
const IDENTITY_POLICY: &str = concat!(
"[[rule]]\nname = \"my-own-reports\"\naction = \"restrict\"\n",
"capabilities = [\"read\", \"comments\", \"history\", \"attachments\"]\n",
"operations = [\"access\"]\n",
"[rule.match]\ncreated_by_me = true\n",
"[[rule]]\nname = \"group-restricted\"\naction = \"deny\"\n",
"[rule.match]\ngroup_restricted = true\n",
);
const NO_IDENTITY_POLICY: &str = concat!(
"[[rule]]\nname = \"group-restricted\"\naction = \"deny\"\n",
"[rule.match]\ngroup_restricted = true\n",
);
const DECLARED_IDENTITY_POLICY: &str = concat!(
"[global]\n",
"identity_source = \"declared\"\n",
"identity_login = \"svc@example.com\"\n",
"[[rule]]\nname = \"my-own-reports\"\naction = \"restrict\"\n",
"capabilities = [\"read\", \"comments\", \"history\", \"attachments\"]\n",
"operations = [\"access\"]\n",
"[rule.match]\ncreated_by_me = true\n",
"[[rule]]\nname = \"group-restricted\"\naction = \"deny\"\n",
"[rule.match]\ngroup_restricted = true\n",
);
async fn mount_whoami(mock: &MockServer, login: &str, hits: u64) {
Mock::given(method("GET"))
.and(path("/rest/whoami"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 1, "name": login, "real_name": "Reporter",
})))
.expect(hits)
.mount(mock)
.await;
}
async fn mount_valid_login(mock: &MockServer, login: &str, result: bool, hits: u64) {
Mock::given(method("GET"))
.and(path("/rest/valid_login"))
.and(query_param("login", login))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"result": result,
})))
.expect(hits)
.mount(mock)
.await;
}
fn server_custody_server(policy: &str, mock: &MockServer) -> BugWarden {
let cfg: Arc<Cli> = Arc::new(pinned(&[
"bugwarden",
"--bugzilla-server",
&mock.uri(),
"--transport",
"stdio",
"--api-key",
"test-key",
]));
let guard = Arc::new(Guard {
policy: Policy::from_toml_str(policy).expect("test policy must parse"),
});
let bz =
Arc::new(BugzillaClient::new(&mock.uri(), false, USER_AGENT).expect("client must build"));
BugWarden::new(cfg, guard, bz).expect("server must build")
}
fn per_request_custody_server(policy: &str, mock: &MockServer) -> BugWarden {
let cfg: Arc<Cli> = Arc::new(pinned(&[
"bugwarden",
"--bugzilla-server",
&mock.uri(),
"--transport",
"http",
]));
let guard = Arc::new(Guard {
policy: Policy::from_toml_str(policy).expect("test policy must parse"),
});
let bz =
Arc::new(BugzillaClient::new(&mock.uri(), false, USER_AGENT).expect("client must build"));
BugWarden::new(cfg, guard, bz).expect("server must build")
}
#[tokio::test]
async fn preflight_fails_closed_when_whoami_is_unavailable_under_server_custody() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/whoami"))
.respond_with(ResponseTemplate::new(404))
.mount(&mock)
.await;
let server = server_custody_server(IDENTITY_POLICY, &mock);
let err = server
.preflight()
.await
.expect_err("a missing whoami endpoint must fail preflight");
let msg = format!("{err:#}");
assert!(
msg.contains("GET /rest/whoami"),
"preflight error must name the failing endpoint: {msg}"
);
assert!(
msg.contains("created_by_me"),
"preflight error must say WHY it matters: {msg}"
);
}
#[tokio::test]
async fn preflight_succeeds_when_whoami_answers_under_server_custody() {
let mock = MockServer::start().await;
mount_whoami(&mock, "reporter@example.com", 1).await;
let server = server_custody_server(IDENTITY_POLICY, &mock);
server
.preflight()
.await
.expect("a working whoami must pass preflight");
}
#[tokio::test]
async fn preflight_costs_zero_requests_without_an_identity_policy() {
let mock = MockServer::start().await;
mount_whoami(&mock, "reporter@example.com", 0).await;
let server = server_custody_server(NO_IDENTITY_POLICY, &mock);
server
.preflight()
.await
.expect("a policy without identity criteria must never probe whoami");
}
#[tokio::test]
async fn preflight_warns_but_succeeds_under_per_request_custody() {
let mock = MockServer::start().await;
mount_whoami(&mock, "reporter@example.com", 0).await;
let server = per_request_custody_server(IDENTITY_POLICY, &mock);
server
.preflight()
.await
.expect("per-request custody must not fail preflight");
}
#[tokio::test]
async fn preflight_succeeds_for_a_correct_declared_login_with_zero_whoami_hits() {
let mock = MockServer::start().await;
mount_valid_login(&mock, "svc@example.com", true, 1).await;
mount_whoami(&mock, "svc@example.com", 0).await;
let server = server_custody_server(DECLARED_IDENTITY_POLICY, &mock);
server
.preflight()
.await
.expect("a key that authenticates as the declared login must pass preflight");
}
#[tokio::test]
async fn preflight_fails_closed_when_the_key_does_not_own_the_declared_login() {
let mock = MockServer::start().await;
mount_valid_login(&mock, "svc@example.com", false, 1).await;
let server = server_custody_server(DECLARED_IDENTITY_POLICY, &mock);
let err = server
.preflight()
.await
.expect_err("a login the key does not own must fail preflight");
let msg = format!("{err:#}");
assert!(
msg.contains("svc@example.com"),
"preflight error must name the declared login: {msg}"
);
}
#[tokio::test]
async fn preflight_declared_login_transport_error_names_the_endpoint() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/valid_login"))
.respond_with(ResponseTemplate::new(500))
.mount(&mock)
.await;
let server = server_custody_server(DECLARED_IDENTITY_POLICY, &mock);
let err = server
.preflight()
.await
.expect_err("a failing valid_login endpoint must fail preflight");
let msg = format!("{err:#}");
assert!(
msg.contains("GET /rest/valid_login"),
"preflight error must name the failing endpoint: {msg}"
);
}
#[tokio::test]
async fn preflight_transport_error_does_not_leak_the_api_key_i12() {
let base = refused::refused_base_url();
let cfg: Arc<Cli> = Arc::new(pinned(&[
"bugwarden",
"--bugzilla-server",
&base,
"--transport",
"stdio",
"--api-key",
"SUPERSECRETKEY123",
]));
let guard = Arc::new(Guard {
policy: Policy::from_toml_str(IDENTITY_POLICY).expect("test policy must parse"),
});
let bz = Arc::new(BugzillaClient::new(&base, false, USER_AGENT).expect("client must build"));
let server = BugWarden::new(cfg, guard, bz).expect("server must build");
let err = tokio::time::timeout(common::REFUSED_CONNECT_BUDGET, server.preflight())
.await
.expect("connect to the refused privileged port must not hang")
.expect_err("an unreachable whoami endpoint must fail preflight");
let msg = format!("{err:#} {err:?}");
assert!(
msg.contains("error sending request"),
"expected a whoami transport error (I12), got: {msg}"
);
assert!(
!msg.contains("SUPERSECRETKEY123"),
"API key leaked into a preflight error: {msg}"
);
}