swarm-controller: inject the account store so link-route tests use the fake
The seven linked-account handlers (matrix, forge and github link and delete, and the list) read their store from `AppState::accounts` instead of calling `store::connect()` themselves. In production that is `Accounts::Live`, which still logs in to the swarm's secret store on every request: a `SecretStore` holds a token it never renews, and a store that is down at startup must fail only the requests made while it is down. Tests set `Accounts::Fake` over the in-memory `FakeStore`. The eight handler tests that proved validation passed by expecting the store connect to fail with 500 now assert that the write or delete landed in the fake, and lose their `BAO_*`-must-be-unset guards: with `BAO_*` set they no longer reach a real store.
This commit is contained in:
parent
c531d39537
commit
dceb803bca
5 changed files with 184 additions and 98 deletions
|
|
@ -104,6 +104,78 @@ impl AccountStore for SecretStore {
|
|||
}
|
||||
}
|
||||
|
||||
/// The store the link, delete and list routes use, as [`AppState`] holds it.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Accounts {
|
||||
/// The swarm's secret store, logged in to afresh on every request: a
|
||||
/// [`SecretStore`] holds a token it never renews, and a store that is down
|
||||
/// fails only the requests made while it is down.
|
||||
Live,
|
||||
/// An in-memory store a test reads back.
|
||||
#[cfg(test)]
|
||||
Fake(std::sync::Arc<tests::FakeStore>),
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
/// A store to make one request's calls on.
|
||||
///
|
||||
/// # Errors
|
||||
/// Whatever [`crate::store::connect`] raises.
|
||||
pub(crate) async fn open(&self) -> Result<OpenAccounts, Error> {
|
||||
match self {
|
||||
Self::Live => crate::store::connect()
|
||||
.await
|
||||
.map(|s| OpenAccounts::Live(Box::new(s))),
|
||||
#[cfg(test)]
|
||||
Self::Fake(store) => Ok(OpenAccounts::Fake(store.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One request's store, from [`Accounts::open`].
|
||||
pub(crate) enum OpenAccounts {
|
||||
Live(Box<SecretStore>),
|
||||
#[cfg(test)]
|
||||
Fake(std::sync::Arc<tests::FakeStore>),
|
||||
}
|
||||
|
||||
impl AccountStore for OpenAccounts {
|
||||
async fn list(&self, dir: &str) -> Result<Vec<String>, Error> {
|
||||
match self {
|
||||
Self::Live(s) => s.list(dir).await,
|
||||
#[cfg(test)]
|
||||
Self::Fake(s) => s.list(dir).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_optional<T: DeserializeOwned + Send>(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> Result<Option<T>, Error> {
|
||||
match self {
|
||||
Self::Live(s) => s.read_optional(path).await,
|
||||
#[cfg(test)]
|
||||
Self::Fake(s) => s.read_optional(path).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn write<T: Serialize + Sync>(&self, path: &str, value: &T) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::Live(s) => s.write(path, value).await,
|
||||
#[cfg(test)]
|
||||
Self::Fake(s) => s.write(path, value).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_all_versions(&self, path: &str) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::Live(s) => s.delete_all_versions(path).await,
|
||||
#[cfg(test)]
|
||||
Self::Fake(s) => s.delete_all_versions(path).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The object names directly under `dir`. A key ending in `/` is a directory
|
||||
/// below it, not an account.
|
||||
async fn objects(store: &impl AccountStore, dir: &str) -> Result<Vec<String>, Error> {
|
||||
|
|
@ -192,7 +264,7 @@ pub async fn get_linked_accounts(
|
|||
.map_err(|reason| error_problem(StatusCode::BAD_REQUEST, reason))?
|
||||
.into_string();
|
||||
|
||||
let store = crate::store::connect().await.map_err(|e| {
|
||||
let store = state.accounts.open().await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "connecting to the swarm secret store failed");
|
||||
error_problem(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
})?;
|
||||
|
|
@ -383,7 +455,7 @@ pub async fn delete_matrix_account(
|
|||
));
|
||||
}
|
||||
|
||||
let store = crate::store::connect().await.map_err(|e| {
|
||||
let store = state.accounts.open().await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "connecting to the swarm secret store failed");
|
||||
error_problem(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
})?;
|
||||
|
|
@ -433,7 +505,7 @@ pub async fn delete_forge_account(
|
|||
let path = forge::account_path(&agent, &label)
|
||||
.map_err(|e| error_problem(StatusCode::BAD_REQUEST, &e.to_string()))?;
|
||||
|
||||
let store = crate::store::connect().await.map_err(|e| {
|
||||
let store = state.accounts.open().await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "connecting to the swarm secret store failed");
|
||||
error_problem(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
})?;
|
||||
|
|
@ -475,7 +547,7 @@ pub async fn delete_github_account(
|
|||
let path = github::account_path(&agent)
|
||||
.map_err(|e| error_problem(StatusCode::BAD_REQUEST, &e.to_string()))?;
|
||||
|
||||
let store = crate::store::connect().await.map_err(|e| {
|
||||
let store = state.accounts.open().await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "connecting to the swarm secret store failed");
|
||||
error_problem(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
})?;
|
||||
|
|
@ -493,7 +565,7 @@ pub async fn delete_github_account(
|
|||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
|
@ -874,7 +946,7 @@ pub(crate) mod tests {
|
|||
}
|
||||
|
||||
/// Bare-minimum `AppState`, as `matrix_account`'s tests build it.
|
||||
fn state() -> super::super::AppState {
|
||||
fn state(store: &Arc<FakeStore>) -> super::super::AppState {
|
||||
super::super::AppState {
|
||||
hives: std::sync::Arc::new(vec![super::super::HiveEntry {
|
||||
name: "pr1ma".to_owned(),
|
||||
|
|
@ -895,30 +967,29 @@ pub(crate) mod tests {
|
|||
auth: None,
|
||||
forge: None,
|
||||
create_gate: std::sync::Arc::default(),
|
||||
accounts: super::Accounts::Fake(Arc::clone(store)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_matrix(account: &str) -> problem_details::ProblemDetails {
|
||||
async fn delete_matrix(
|
||||
store: &Arc<FakeStore>,
|
||||
account: &str,
|
||||
revoke: bool,
|
||||
) -> Result<axum::http::StatusCode, problem_details::ProblemDetails> {
|
||||
super::delete_matrix_account(
|
||||
axum::extract::State(state()),
|
||||
axum::extract::State(state(store)),
|
||||
axum::extract::Path(("pr1ma".to_owned(), "atlas".to_owned(), account.to_owned())),
|
||||
axum::extract::Query(super::DeleteMatrixAccountQuery { revoke: true }),
|
||||
axum::extract::Query(super::DeleteMatrixAccountQuery { revoke }),
|
||||
)
|
||||
.await
|
||||
.expect_err("no store is configured in a test")
|
||||
}
|
||||
|
||||
/// Refused before the store, and before any revoke: with `BAO_*` unset a
|
||||
/// store connect would answer 500.
|
||||
/// Refused before the store, and before any revoke.
|
||||
#[tokio::test]
|
||||
async fn the_agents_own_matrix_account_cannot_be_deleted() {
|
||||
for var in ["BAO_ADDR", "BAO_CLIENT_CERT", "BAO_CLIENT_KEY"] {
|
||||
assert!(
|
||||
std::env::var(var).is_err(),
|
||||
"{var} must be unset for this test to prove anything"
|
||||
);
|
||||
}
|
||||
let problem = delete_matrix("main").await;
|
||||
let problem = delete_matrix(&Arc::default(), "main", true)
|
||||
.await
|
||||
.expect_err("'main' is reserved");
|
||||
assert_eq!(
|
||||
problem.status,
|
||||
Some(axum::http::StatusCode::BAD_REQUEST),
|
||||
|
|
@ -926,14 +997,14 @@ pub(crate) mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// The control: any other account name reaches the store connect.
|
||||
/// The control: any other account name is deleted.
|
||||
#[tokio::test]
|
||||
async fn another_matrix_account_reaches_the_store() {
|
||||
let problem = delete_matrix("catgirl").await;
|
||||
assert_eq!(
|
||||
problem.status,
|
||||
Some(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
"{problem:?}"
|
||||
);
|
||||
let store = Arc::new(every_kind());
|
||||
let status = delete_matrix(&store, "catgirl", false)
|
||||
.await
|
||||
.expect("stored");
|
||||
assert_eq!(status, axum::http::StatusCode::NO_CONTENT);
|
||||
assert_eq!(deleted(&store), ["swarm/agents/atlas/matrix/catgirl"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue