mod common;
use common::{read_problem, spawn_test_server, test_client, unique_stream_name};
#[tokio::test]
async fn test_append_returns_204() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.body("Hello, world!")
.send()
.await
.unwrap();
assert_eq!(response.status(), 204, "Expected 204 No Content");
let next_offset = response
.headers()
.get("Stream-Next-Offset")
.expect("Missing Stream-Next-Offset header")
.to_str()
.unwrap();
assert_eq!(next_offset, "0000000000000001_000000000000000d");
let body = response.bytes().await.unwrap();
assert!(body.is_empty(), "POST response should have no body");
}
#[tokio::test]
async fn test_multiple_appends_monotonic_offsets() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response1 = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.body("first")
.send()
.await
.unwrap();
let offset1 = response1
.headers()
.get("Stream-Next-Offset")
.unwrap()
.to_str()
.unwrap();
assert_eq!(offset1, "0000000000000001_0000000000000005");
let response2 = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.body("second")
.send()
.await
.unwrap();
let offset2 = response2
.headers()
.get("Stream-Next-Offset")
.unwrap()
.to_str()
.unwrap();
assert_eq!(offset2, "0000000000000002_000000000000000b");
let response3 = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.body("third")
.send()
.await
.unwrap();
let offset3 = response3
.headers()
.get("Stream-Next-Offset")
.unwrap()
.to_str()
.unwrap();
assert_eq!(offset3, "0000000000000003_0000000000000010");
}
#[tokio::test]
async fn test_append_content_type_mismatch_returns_409() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "application/json")
.body(r#"{"key": "value"}"#)
.send()
.await
.unwrap();
assert_eq!(response.status(), 409, "Expected 409 Conflict");
}
#[tokio::test]
async fn test_append_content_type_case_insensitive() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "TEXT/PLAIN")
.body("data")
.send()
.await
.unwrap();
assert_eq!(
response.status(),
204,
"Expected 204 for case-insensitive match"
);
}
#[tokio::test]
async fn test_append_content_type_ignores_charset() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain; charset=utf-8")
.body("data")
.send()
.await
.unwrap();
assert_eq!(
response.status(),
204,
"Expected 204 when charset is ignored"
);
}
#[tokio::test]
async fn test_append_empty_body_without_closed_returns_400() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
assert_eq!(
response.status(),
400,
"Expected 400 for empty body without Stream-Closed"
);
}
#[tokio::test]
async fn test_close_stream_without_data() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.header("Stream-Closed", "true")
.send()
.await
.unwrap();
assert_eq!(response.status(), 204, "Expected 204 for close operation");
let head_response = client
.head(format!("{base_url}/v1/stream/{stream_name}"))
.send()
.await
.unwrap();
let closed = head_response
.headers()
.get("Stream-Closed")
.expect("Missing Stream-Closed header")
.to_str()
.unwrap();
assert_eq!(closed, "true");
}
#[tokio::test]
async fn test_close_stream_with_data() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.header("Stream-Closed", "true")
.body("goodbye")
.send()
.await
.unwrap();
assert_eq!(response.status(), 204, "Expected 204 for close with data");
let next_offset = response
.headers()
.get("Stream-Next-Offset")
.unwrap()
.to_str()
.unwrap();
assert_eq!(next_offset, "0000000000000001_0000000000000007");
}
#[tokio::test]
async fn test_append_to_closed_stream_returns_409() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
client
.put(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.send()
.await
.unwrap();
client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.header("Stream-Closed", "true")
.send()
.await
.unwrap();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.body("more data")
.send()
.await
.unwrap();
assert_eq!(response.status(), 409, "Expected 409 Conflict");
let closed = response
.headers()
.get("Stream-Closed")
.expect("Missing Stream-Closed header in error")
.to_str()
.unwrap();
assert_eq!(closed, "true");
let problem = read_problem(response).await;
let instance = format!("/v1/stream/{stream_name}");
assert_eq!(problem.problem_type, "/errors/stream-closed");
assert_eq!(problem.title, "Stream Closed");
assert_eq!(problem.status, 409);
assert_eq!(problem.code, "STREAM_CLOSED");
assert_eq!(problem.instance.as_deref(), Some(instance.as_str()));
}
#[tokio::test]
async fn test_append_to_nonexistent_stream_returns_404() {
let (base_url, _port) = spawn_test_server().await;
let client = test_client();
let stream_name = unique_stream_name();
let response = client
.post(format!("{base_url}/v1/stream/{stream_name}"))
.header("Content-Type", "text/plain")
.body("data")
.send()
.await
.unwrap();
assert_eq!(response.status(), 404, "Expected 404 Not Found");
let problem = read_problem(response).await;
let instance = format!("/v1/stream/{stream_name}");
assert_eq!(problem.problem_type, "/errors/not-found");
assert_eq!(problem.title, "Stream Not Found");
assert_eq!(problem.status, 404);
assert_eq!(problem.code, "NOT_FOUND");
assert_eq!(problem.instance.as_deref(), Some(instance.as_str()));
}