use std::sync::Arc;
use axum_test::TestServer;
use serde_json::{json, Value};
use sirr_server::{
router,
store::{crypto, Store, Visibility},
AppState, WebhookSender,
};
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn make_state(store: Arc<Store>) -> AppState {
let key = Arc::new(crypto::generate_key());
let visibility = Arc::new(tokio::sync::RwLock::new(Visibility::Both));
AppState {
store,
encryption_key: key,
visibility,
webhook_sender: WebhookSender::new(),
base_url: "http://test".to_string(),
}
}
fn auth_header(
token: &str,
) -> (
axum::http::header::HeaderName,
axum::http::header::HeaderValue,
) {
(
axum::http::header::AUTHORIZATION,
format!("Bearer {token}").parse().unwrap(),
)
}
#[tokio::test]
async fn webhook_fires_on_create() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/hook"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock_server)
.await;
let dir = tempdir().unwrap();
let store = Arc::new(Store::open(dir.path().join("test.db")).unwrap());
let webhook_url = format!("{}/hook", mock_server.uri());
let (_key, token) = store
.create_key("alice", None, None, Some(webhook_url))
.unwrap();
let state = make_state(store);
let server = TestServer::new(router(state));
let resp = server
.post("/secret")
.add_header(auth_header(&token).0, auth_header(&token).1)
.json(&json!({"value": "top-secret"}))
.await;
resp.assert_status_success();
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
mock_server.verify().await;
}
#[tokio::test]
async fn webhook_fires_on_read() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/hook"))
.respond_with(ResponseTemplate::new(200))
.expect(2) .mount(&mock_server)
.await;
let dir = tempdir().unwrap();
let store = Arc::new(Store::open(dir.path().join("test.db")).unwrap());
let webhook_url = format!("{}/hook", mock_server.uri());
let (_key, token) = store
.create_key("bob", None, None, Some(webhook_url))
.unwrap();
let state = make_state(store);
let server = TestServer::new(router(state));
let resp = server
.post("/secret")
.add_header(auth_header(&token).0, auth_header(&token).1)
.json(&json!({"value": "read-me"}))
.await;
resp.assert_status_success();
let hash = resp.json::<Value>()["hash"].as_str().unwrap().to_string();
let _read_resp = server.get(&format!("/secret/{hash}")).await;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
mock_server.verify().await;
}
#[tokio::test]
async fn webhook_does_not_fire_for_anonymous_secrets() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200))
.expect(0)
.mount(&mock_server)
.await;
let dir = tempdir().unwrap();
let store = Arc::new(Store::open(dir.path().join("test.db")).unwrap());
let state = make_state(store);
let server = TestServer::new(router(state));
let resp = server
.post("/secret")
.json(&json!({"value": "anon-secret"}))
.await;
resp.assert_status_success();
let hash = resp.json::<Value>()["hash"].as_str().unwrap().to_string();
let _read_resp = server.get(&format!("/secret/{hash}")).await;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
mock_server.verify().await;
}
#[tokio::test]
async fn webhook_does_not_fire_when_no_url_configured() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200))
.expect(0)
.mount(&mock_server)
.await;
let dir = tempdir().unwrap();
let store = Arc::new(Store::open(dir.path().join("test.db")).unwrap());
let (_key, token) = store.create_key("carol", None, None, None).unwrap();
let state = make_state(store);
let server = TestServer::new(router(state));
let resp = server
.post("/secret")
.add_header(auth_header(&token).0, auth_header(&token).1)
.json(&json!({"value": "no-webhook"}))
.await;
resp.assert_status_success();
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
mock_server.verify().await;
}
#[tokio::test]
async fn webhook_payload_shape() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/hook"))
.respond_with(ResponseTemplate::new(200))
.mount(&mock_server)
.await;
let dir = tempdir().unwrap();
let store = Arc::new(Store::open(dir.path().join("test.db")).unwrap());
let webhook_url = format!("{}/hook", mock_server.uri());
let (_key, token) = store
.create_key("dave", None, None, Some(webhook_url))
.unwrap();
let state = make_state(store);
let server = TestServer::new(router(state));
let resp = server
.post("/secret")
.add_header(auth_header(&token).0, auth_header(&token).1)
.json(&json!({"value": "payload-check"}))
.await;
resp.assert_status_success();
let hash = resp.json::<Value>()["hash"].as_str().unwrap().to_string();
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
let received = mock_server.received_requests().await.unwrap();
assert!(
!received.is_empty(),
"expected at least one webhook request"
);
let body: Value =
serde_json::from_slice(&received[0].body).expect("webhook body should be JSON");
assert_eq!(body["type"], json!("secret.created"));
assert_eq!(body["hash"], json!(hash));
assert!(
body["at"].as_i64().is_some(),
"at should be a unix timestamp"
);
assert!(body["ip"].is_string(), "ip field should be present");
}