use crate::AvisoClient;
use crate::client::parse_json_response_optional;
impl AvisoClient {
pub async fn wipe_stream(&self, stream_name: &str) -> crate::Result<()> {
let url = self.endpoint("api/v1/admin/wipe/stream")?;
let body = serde_json::json!({ "stream_name": stream_name });
let response = self
.send_with_refresh(|http| http.delete(url.clone()).json(&body))
.await?;
parse_json_response_optional(response).await
}
pub async fn wipe_all(&self) -> crate::Result<()> {
let url = self.endpoint("api/v1/admin/wipe/all")?;
let response = self
.send_with_refresh(|http| http.delete(url.clone()))
.await?;
parse_json_response_optional(response).await
}
pub async fn delete_notification(&self, notification_id: &str) -> crate::Result<()> {
crate::client::validate_path_segment(notification_id)?;
let url = self.endpoint(&format!("api/v1/admin/notification/{notification_id}"))?;
let response = self
.send_with_refresh(|http| http.delete(url.clone()))
.await?;
parse_json_response_optional(response).await
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::panic,
reason = "test code: unwrap on constructor success and panic on unexpected variant are the standard test diagnostics"
)]
mod tests {
use std::sync::Arc;
use serde_json::json;
use wiremock::matchers::{body_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use crate::auth::{AuthProvider, Bearer};
use crate::{AvisoClient, ClientError};
fn client_for(server: &MockServer, auth: Option<Arc<dyn AuthProvider>>) -> AvisoClient {
let mut builder = AvisoClient::builder().base_url(server.uri());
if let Some(a) = auth {
builder = builder.auth(a);
}
builder.build().unwrap()
}
#[tokio::test]
async fn wipe_stream_sends_stream_name_in_body() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/admin/wipe/stream"))
.and(body_json(json!({ "stream_name": "mars" })))
.respond_with(ResponseTemplate::new(200).set_body_string(""))
.expect(1)
.mount(&server)
.await;
let client = client_for(&server, None);
client.wipe_stream("mars").await.unwrap();
}
#[tokio::test]
async fn wipe_all_hits_correct_path_and_method() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/admin/wipe/all"))
.respond_with(ResponseTemplate::new(204))
.expect(1)
.mount(&server)
.await;
let client = client_for(&server, None);
client.wipe_all().await.unwrap();
}
#[tokio::test]
async fn delete_notification_rejects_path_traversal_attempt() {
let server = MockServer::start().await;
let client = client_for(&server, None);
let err = client.delete_notification("../wipe/all").await.unwrap_err();
assert!(
matches!(err, ClientError::Config(_)),
"expected Config validation error, got {err:?}"
);
}
#[tokio::test]
async fn delete_notification_rejects_slash_in_id() {
let server = MockServer::start().await;
let client = client_for(&server, None);
let err = client.delete_notification("mars/42").await.unwrap_err();
assert!(matches!(err, ClientError::Config(_)), "got {err:?}");
}
#[tokio::test]
async fn delete_notification_rejects_empty_id() {
let server = MockServer::start().await;
let client = client_for(&server, None);
let err = client.delete_notification("").await.unwrap_err();
assert!(matches!(err, ClientError::Config(_)), "got {err:?}");
}
#[tokio::test]
async fn delete_notification_keeps_at_sign_in_path_verbatim() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/admin/notification/mars@42"))
.respond_with(ResponseTemplate::new(200).set_body_string(""))
.expect(1)
.mount(&server)
.await;
let client = client_for(&server, None);
client.delete_notification("mars@42").await.unwrap();
}
#[tokio::test]
async fn delete_notification_surfaces_404() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/admin/notification/missing@1"))
.respond_with(ResponseTemplate::new(404).set_body_string("not found"))
.mount(&server)
.await;
let client = client_for(&server, None);
let err = client.delete_notification("missing@1").await.unwrap_err();
match err {
ClientError::Http { status, body, .. } => {
assert_eq!(status, 404);
assert!(body.contains("not found"), "body={body}");
}
other => panic!("expected Http(404), got {other:?}"),
}
}
#[tokio::test]
async fn admin_refreshes_and_retries_once_on_401() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/admin/wipe/all"))
.respond_with(ResponseTemplate::new(401))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/api/v1/admin/wipe/all"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let auth: Arc<dyn AuthProvider> = Arc::new(Bearer::new("tok").unwrap());
let client = client_for(&server, Some(auth));
client.wipe_all().await.unwrap();
}
}