#[path = "../src/generated/test_flatbed.rs"]
#[allow(warnings, clippy::all)]
mod generated;
mod common;
use generated::test::{TestRequest, TestResponse};
use flatbed::{
get_routes, get_workers, route, validate_routes, FlatbedRouteError, FlatbedWorkerError,
Request, Response, Worker,
};
use std::any::Any;
use std::sync::Arc;
fn dummy_ctx() -> Arc<dyn Any + Send + Sync> {
Arc::new(())
}
#[route("/api/ping")]
async fn test_ping_handler(
req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
let msg = req.body.message.as_deref().unwrap_or("no message");
Ok(Response::ok(TestResponse {
message: Some(format!("Test pong: {}", msg)),
value: req.body.value + 100,
success: true,
}))
}
#[route("/api/health")]
async fn test_health_handler(
req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
Ok(Response::ok(TestResponse {
message: Some("Healthy".to_string()),
value: req.body.value,
success: true,
}))
}
#[route("/api/error")]
async fn test_error_handler(
_req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
Err(FlatbedRouteError::bad_request("Test error message").code("TEST_ERROR"))
}
#[route("/api/resource", method = "GET", tag = "Resources")]
async fn test_get_handler(
req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
Ok(Response::ok(TestResponse {
message: Some("Got resource".to_string()),
value: req.body.value,
success: true,
}))
}
#[route("/api/resource", method = "PUT", tag = "Resources")]
async fn test_put_handler(
req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
Ok(Response::ok(TestResponse {
message: Some("Updated resource".to_string()),
value: req.body.value,
success: true,
}))
}
#[route("/api/with-headers")]
async fn test_response_headers_handler(
req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
Ok(Response::ok(TestResponse {
message: Some("Response with headers".to_string()),
value: req.body.value,
success: true,
})
.header("x-custom-header", "custom-value")
.header("x-another-header", "another-value"))
}
#[route("/api/error-with-details")]
async fn test_error_with_details_handler(
_req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError<TestResponse>> {
Err(FlatbedRouteError::bad_request("Validation failed")
.code("VALIDATION_ERROR")
.header("x-error-source", "validation")
.with_details(TestResponse {
message: Some("field 'email' is invalid".to_string()),
value: 42,
success: false,
}))
}
#[route("/api/error-with-headers")]
async fn test_error_with_headers_handler(
_req: Request<TestRequest>,
) -> Result<Response<TestResponse>, FlatbedRouteError> {
Err(FlatbedRouteError::not_found("Resource not found")
.code("NOT_FOUND")
.header("x-error-type", "not_found")
.header("x-retry-after", "60"))
}
#[test]
fn test_route_validation() {
let result = validate_routes();
assert!(result.is_ok(), "Route validation should pass");
}
#[test]
fn test_route_registration() {
let routes = get_routes();
assert!(
routes.contains_key(&("/api/ping".to_string(), flatbed::HttpMethod::Post)),
"Route POST /api/ping should be registered"
);
assert!(
routes.contains_key(&("/api/health".to_string(), flatbed::HttpMethod::Post)),
"Route POST /api/health should be registered"
);
let ping_route = routes
.get(&("/api/ping".to_string(), flatbed::HttpMethod::Post))
.unwrap();
assert_eq!(ping_route.path, "/api/ping");
assert_eq!(ping_route.method, flatbed::HttpMethod::Post);
assert_eq!(ping_route.request_type, "TestRequest");
let get_route = routes
.get(&("/api/resource".to_string(), flatbed::HttpMethod::Get))
.unwrap();
assert_eq!(get_route.method, flatbed::HttpMethod::Get);
let put_route = routes
.get(&("/api/resource".to_string(), flatbed::HttpMethod::Put))
.unwrap();
assert_eq!(put_route.method, flatbed::HttpMethod::Put);
}
#[tokio::test]
async fn test_hyper_handler_json() {
let request = TestRequest {
message: Some("Integration test message".to_string()),
value: 9999,
};
let request_bytes = flatbed::serde_json::to_vec(&request).unwrap();
let parts = flatbed::RequestParts::new(flatbed::Method::POST, "/api/ping".to_string());
let result =
__hyper_handler_test_ping_handler(parts, request_bytes, "application/json", dummy_ctx())
.await;
assert!(result.is_ok(), "Handler should succeed");
let response_parts = result.unwrap();
assert_eq!(response_parts.status, flatbed::StatusCode::OK);
assert_eq!(response_parts.content_type, "application/json");
let response: TestResponse =
flatbed::serde_json::from_slice(&response_parts.body).expect("Should deserialize response");
assert!(response.success);
assert_eq!(response.value, 9999 + 100);
assert!(response
.message
.as_deref()
.unwrap()
.contains("Integration test message"));
}
#[tokio::test]
async fn test_hyper_handler_flatbuffer() {
let request = TestRequest {
message: Some("FlatBuffer test".to_string()),
value: 1234,
};
let request_bytes = request.to_flatbuffer();
let parts = flatbed::RequestParts::new(flatbed::Method::POST, "/api/ping".to_string());
let result = __hyper_handler_test_ping_handler(
parts,
request_bytes,
"application/x-flatbuffers",
dummy_ctx(),
)
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(response_parts.content_type, "application/x-flatbuffers");
let response =
TestResponse::from_flatbuffer(&response_parts.body).expect("Should deserialize response");
assert!(response.success);
assert_eq!(response.value, 1234 + 100);
}
#[tokio::test]
async fn test_error_response_json() {
let request = TestRequest {
message: Some("test".to_string()),
value: 0,
};
let request_bytes = flatbed::serde_json::to_vec(&request).unwrap();
let parts = flatbed::RequestParts::new(flatbed::Method::POST, "/api/error".to_string());
let result =
__hyper_handler_test_error_handler(parts, request_bytes, "application/json", dummy_ctx())
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(response_parts.status, flatbed::StatusCode::BAD_REQUEST);
let error_body: serde_json::Value =
serde_json::from_slice(&response_parts.body).expect("Should deserialize error");
assert_eq!(error_body["code"], "TEST_ERROR");
assert_eq!(error_body["message"], "Test error message");
}
#[tokio::test]
async fn test_request_id_propagation() {
let request = TestRequest {
message: Some("test".to_string()),
value: 0,
};
let request_bytes = flatbed::serde_json::to_vec(&request).unwrap();
let mut parts = flatbed::RequestParts::new(flatbed::Method::POST, "/api/ping".to_string());
parts.headers.insert(
"x-request-id",
flatbed::HeaderValue::from_static("custom-request-id-123"),
);
parts = parts.with_request_id_from_header();
let result =
__hyper_handler_test_ping_handler(parts, request_bytes, "application/json", dummy_ctx())
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(
response_parts
.headers
.get("x-request-id")
.and_then(|v| v.to_str().ok()),
Some("custom-request-id-123")
);
}
#[tokio::test]
async fn test_response_with_custom_headers() {
let request = TestRequest {
message: Some("test".to_string()),
value: 123,
};
let request_bytes = flatbed::serde_json::to_vec(&request).unwrap();
let parts = flatbed::RequestParts::new(flatbed::Method::POST, "/api/with-headers".to_string());
let result = __hyper_handler_test_response_headers_handler(
parts,
request_bytes,
"application/json",
dummy_ctx(),
)
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(
response_parts
.headers
.get("x-custom-header")
.and_then(|v| v.to_str().ok()),
Some("custom-value")
);
assert_eq!(
response_parts
.headers
.get("x-another-header")
.and_then(|v| v.to_str().ok()),
Some("another-value")
);
}
#[tokio::test]
async fn test_error_with_details() {
let request = TestRequest {
message: Some("test".to_string()),
value: 0,
};
let request_bytes = flatbed::serde_json::to_vec(&request).unwrap();
let parts =
flatbed::RequestParts::new(flatbed::Method::POST, "/api/error-with-details".to_string());
let result = __hyper_handler_test_error_with_details_handler(
parts,
request_bytes,
"application/json",
dummy_ctx(),
)
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(response_parts.status, flatbed::StatusCode::BAD_REQUEST);
assert_eq!(
response_parts
.headers
.get("x-error-source")
.and_then(|v| v.to_str().ok()),
Some("validation")
);
let error_body: serde_json::Value =
serde_json::from_slice(&response_parts.body).expect("Should deserialize error");
assert_eq!(error_body["code"], "VALIDATION_ERROR");
assert_eq!(error_body["message"], "Validation failed");
assert!(error_body["details"].is_object());
assert_eq!(error_body["details"]["message"], "field 'email' is invalid");
}
#[tokio::test]
async fn test_error_with_custom_headers() {
let request = TestRequest {
message: Some("test".to_string()),
value: 0,
};
let request_bytes = flatbed::serde_json::to_vec(&request).unwrap();
let parts =
flatbed::RequestParts::new(flatbed::Method::POST, "/api/error-with-headers".to_string());
let result = __hyper_handler_test_error_with_headers_handler(
parts,
request_bytes,
"application/json",
dummy_ctx(),
)
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(response_parts.status, flatbed::StatusCode::NOT_FOUND);
assert_eq!(
response_parts
.headers
.get("x-error-type")
.and_then(|v| v.to_str().ok()),
Some("not_found")
);
assert_eq!(
response_parts
.headers
.get("x-retry-after")
.and_then(|v| v.to_str().ok()),
Some("60")
);
}
#[tokio::test]
async fn test_flatbuffer_hyphenated_content_type() {
let request = TestRequest {
message: Some("Hyphenated content type test".to_string()),
value: 5678,
};
let request_bytes = request.to_flatbuffer();
let parts = flatbed::RequestParts::new(flatbed::Method::POST, "/api/ping".to_string());
let result = __hyper_handler_test_ping_handler(
parts,
request_bytes,
"application/x-flat-buffers",
dummy_ctx(),
)
.await;
assert!(result.is_ok());
let response_parts = result.unwrap();
assert_eq!(response_parts.content_type, "application/x-flatbuffers");
let response =
TestResponse::from_flatbuffer(&response_parts.body).expect("Should deserialize response");
assert!(response.success);
assert_eq!(response.value, 5678 + 100);
}
#[derive(Default)]
struct TestSimpleWorker;
impl Worker for TestSimpleWorker {
type Context = ();
const NAME: &'static str = "test_simple_worker";
fn run(&self, _ctx: Arc<Self::Context>) -> flatbed::BoxFuture<Result<(), FlatbedWorkerError>> {
Box::pin(async move {
std::future::pending::<()>().await;
Ok(())
})
}
}
flatbed::register_worker!(TestSimpleWorker, ());
#[derive(Default)]
struct TestCustomWorker;
impl Worker for TestCustomWorker {
type Context = ();
const NAME: &'static str = "custom-worker";
const DESCRIPTION: Option<&'static str> = Some("A worker with custom attributes");
fn run(&self, _ctx: Arc<Self::Context>) -> flatbed::BoxFuture<Result<(), FlatbedWorkerError>> {
Box::pin(async move {
std::future::pending::<()>().await;
Ok(())
})
}
}
flatbed::register_worker!(TestCustomWorker, ());
#[test]
fn test_worker_registration() {
let workers = get_workers();
let simple_worker = workers.iter().find(|w| w.name == "test_simple_worker");
assert!(
simple_worker.is_some(),
"Simple worker should be registered"
);
assert!(simple_worker.unwrap().description.is_none());
let custom_worker = workers.iter().find(|w| w.name == "custom-worker");
assert!(
custom_worker.is_some(),
"Custom worker should be registered"
);
assert_eq!(
custom_worker.unwrap().description,
Some("A worker with custom attributes")
);
assert!(simple_worker.unwrap().restart.is_none());
assert!(flatbed::get_worker_drains()
.iter()
.any(|d| d.name == "test_simple_worker"));
}
struct StubTelemetryService;
impl flatbed::TelemetryService for StubTelemetryService {
fn new(_config: flatbed::TelemetryConfig) -> Arc<Self>
where
Self: Sized,
{
Arc::new(Self)
}
fn register_f64_counter(
&self,
_name: &str,
_help: &str,
_labels: Option<std::collections::HashMap<String, String>>,
) -> Result<Arc<dyn flatbed::telemetry::Counter<f64>>, flatbed::TelemetryError> {
Ok(Arc::new(StubCounter))
}
fn register_u64_counter(
&self,
_name: &str,
_help: &str,
_labels: Option<std::collections::HashMap<String, String>>,
) -> Result<Arc<dyn flatbed::telemetry::Counter<u64>>, flatbed::TelemetryError> {
Ok(Arc::new(StubCounter))
}
fn get_feed(&self) -> Result<String, flatbed::TelemetryError> {
Ok(String::new())
}
fn service_name(&self) -> String {
"stub".to_string()
}
fn ip_address(&self) -> String {
"127.0.0.1".to_string()
}
}
struct StubCounter;
impl flatbed::telemetry::Counter<f64> for StubCounter {
fn inc(&self) {}
fn inc_by(&self, _value: f64) {}
}
impl flatbed::telemetry::Counter<u64> for StubCounter {
fn inc(&self) {}
fn inc_by(&self, _value: u64) {}
}
#[tokio::test]
async fn test_boot_lifecycle_probes() {
use flatbed::{Flatbed, FlatbedConfig};
use tokio::sync::oneshot;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let telemetry: Arc<dyn flatbed::TelemetryService> = Arc::new(StubTelemetryService);
let config = FlatbedConfig::new("Test API")
.host("127.0.0.1")
.port(port)
.with_telemetry(telemetry);
let (boot_tx, boot_rx) = oneshot::channel::<()>();
let server_handle = tokio::spawn(async move {
Flatbed::run(config, |_config| async move {
let _ = boot_rx.await;
Ok(()) })
.await
});
let base = format!("http://127.0.0.1:{}", port);
let client = reqwest::Client::new();
for _ in 0..100 {
if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.is_ok()
{
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
let resp = client
.get(format!("{}/healthz", base))
.send()
.await
.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"/healthz should be 200 during boot"
);
let resp = client.get(format!("{}/readyz", base)).send().await.unwrap();
assert_eq!(
resp.status().as_u16(),
503,
"/readyz should be 503 during boot"
);
let resp = client
.get(format!("{}/schema.bfbs", base))
.send()
.await
.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"/schema.bfbs should be 200 during boot"
);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/octet-stream"
);
let bfbs = resp.bytes().await.unwrap();
assert!(!bfbs.is_empty(), "/schema.bfbs must return the reflection");
let resp = client
.post(format!("{}/api/ping", base))
.header("content-type", "application/json")
.body(r#"{"message":"test","value":1}"#)
.send()
.await
.unwrap();
assert_eq!(
resp.status().as_u16(),
503,
"User routes should be 503 during boot"
);
boot_tx.send(()).unwrap();
for _ in 0..100 {
let resp = client.get(format!("{}/readyz", base)).send().await.unwrap();
if resp.status().as_u16() == 200 {
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
let resp = client
.get(format!("{}/healthz", base))
.send()
.await
.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"/healthz should be 200 after boot"
);
let resp = client.get(format!("{}/readyz", base)).send().await.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"/readyz should be 200 after boot"
);
let resp = client
.post(format!("{}/api/ping", base))
.header("content-type", "application/json")
.body(r#"{"message":"lifecycle test","value":42}"#)
.send()
.await
.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"User routes should be 200 after boot"
);
server_handle.abort();
}
#[tokio::test]
async fn test_readiness_gate_probes() {
use flatbed::{Flatbed, FlatbedConfig, Readiness};
use tokio::sync::oneshot;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let readiness = Readiness::new();
let gate = readiness.gate("broker");
let telemetry: Arc<dyn flatbed::TelemetryService> = Arc::new(StubTelemetryService);
let config = FlatbedConfig::new("Test API")
.host("127.0.0.1")
.port(port)
.readiness(readiness)
.with_telemetry(telemetry);
let (boot_tx, boot_rx) = oneshot::channel::<()>();
let server_handle = tokio::spawn(async move {
Flatbed::run(config, |_config| async move {
let _ = boot_rx.await;
Ok(())
})
.await
});
let base = format!("http://127.0.0.1:{}", port);
let client = reqwest::Client::new();
for _ in 0..100 {
if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.is_ok()
{
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
let readyz = |client: reqwest::Client, base: String| async move {
let resp = client.get(format!("{}/readyz", base)).send().await.unwrap();
(resp.status().as_u16(), resp.text().await.unwrap())
};
let ping = |client: reqwest::Client, base: String| async move {
let resp = client
.post(format!("{}/api/ping", base))
.header("content-type", "application/json")
.body(r#"{"message":"gate","value":1}"#)
.send()
.await
.unwrap();
(resp.status().as_u16(), resp.text().await.unwrap())
};
let (status, body) = readyz(client.clone(), base.clone()).await;
assert_eq!(status, 503, "/readyz is 503 during boot");
assert_eq!(
body, "Not Ready",
"the boot latch, not a gate, holds it down"
);
let (status, body) = ping(client.clone(), base.clone()).await;
assert_eq!(status, 503, "user routes are 503 during boot");
assert!(
body.contains("BOOTING"),
"an unfinished boot reports BOOTING, got {body}"
);
boot_tx.send(()).unwrap();
let mut named = false;
for _ in 0..100 {
if readyz(client.clone(), base.clone()).await.1 == "Not Ready: broker" {
named = true;
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
assert!(named, "/readyz should name the gate holding it down");
let (status, body) = ping(client.clone(), base.clone()).await;
assert_eq!(status, 503, "user routes are 503 behind a closed gate");
assert!(
body.contains("NOT_READY") && body.contains("broker"),
"a closed gate reports NOT_READY and names itself, got {body}"
);
gate.set_ready(true);
assert_eq!(
readyz(client.clone(), base.clone()).await,
(200, "Ready".to_string()),
"an open gate on a booted server is ready"
);
assert_eq!(
ping(client.clone(), base.clone()).await.0,
200,
"user routes serve once the gate opens"
);
gate.set_ready(false);
assert_eq!(
readyz(client.clone(), base.clone()).await,
(503, "Not Ready: broker".to_string()),
"a gate that closes again takes readiness with it"
);
server_handle.abort();
}
#[tokio::test]
async fn test_head_request_builtins_strip_body_keep_length() {
use flatbed::{Flatbed, FlatbedConfig};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let telemetry: Arc<dyn flatbed::TelemetryService> = Arc::new(StubTelemetryService);
let config = FlatbedConfig::new("Test API")
.host("127.0.0.1")
.port(port)
.splash("Test API Server")
.with_telemetry(telemetry);
let server = tokio::spawn(async move { Flatbed::run(config, |_| async { Ok(()) }).await });
let base = format!("http://127.0.0.1:{}", port);
let client = reqwest::Client::new();
let mut ready = false;
for _ in 0..100 {
if let Ok(r) = client.get(format!("{}/readyz", base)).send().await {
if r.status().as_u16() == 200 {
ready = true;
break;
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
assert!(ready, "server did not become ready");
for path in ["/", "/healthz", "/readyz", "/schema.bfbs", "/openapi.json"] {
let url = format!("{}{}", base, path);
let get = client.get(&url).send().await.unwrap();
assert_eq!(get.status().as_u16(), 200, "GET {path} should be 200");
let get_content_length = get
.headers()
.get("content-length")
.expect("GET content-length present")
.to_str()
.unwrap()
.to_string();
let head = client.head(&url).send().await.unwrap();
assert_eq!(head.status().as_u16(), 200, "HEAD {path} should be 200");
assert_eq!(
head.headers()
.get("content-length")
.expect("HEAD content-length present")
.to_str()
.unwrap(),
get_content_length,
"HEAD {path} content-length must equal GET's over HTTP/1.1"
);
let (status, content_length, received) = common::h2c_head_request(port, path).await;
assert_eq!(
status,
http::StatusCode::OK,
"HEAD {path} should be 200 over h2c"
);
assert_eq!(
content_length.as_deref(),
Some(get_content_length.as_str()),
"HEAD {path} content-length must equal GET's over h2c"
);
assert_eq!(
received, 0,
"HEAD {path} must not put a body on the wire over h2c"
);
}
server.abort();
}
#[tokio::test]
#[allow(clippy::result_large_err)]
async fn test_before_request_guard_rejects_before_handler_runs() {
use flatbed::{Flatbed, FlatbedConfig};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let telemetry: Arc<dyn flatbed::TelemetryService> = Arc::new(StubTelemetryService);
let config = FlatbedConfig::new("Test API")
.host("127.0.0.1")
.port(port)
.with_telemetry(telemetry)
.before_request(|req| match req.header("authorization") {
Some("Bearer secret") => Ok(()),
_ => Err(
FlatbedRouteError::unauthorized("missing or invalid bearer token")
.code("UNAUTHORIZED"),
),
});
let server = tokio::spawn(async move { Flatbed::run(config, |_| async { Ok(()) }).await });
let base = format!("http://127.0.0.1:{}", port);
let client = reqwest::Client::new();
let mut ready = false;
for _ in 0..100 {
if let Ok(r) = client.get(format!("{}/readyz", base)).send().await {
if r.status().as_u16() == 200 {
ready = true;
break;
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
assert!(ready, "server did not become ready");
let resp = client
.post(format!("{}/api/ping", base))
.header("content-type", "application/json")
.body(r#"{"message":"hi","value":1}"#)
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 401);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["code"], "UNAUTHORIZED");
assert_eq!(body["message"], "missing or invalid bearer token");
assert!(body.get("value").is_none());
let resp = client
.post(format!("{}/api/ping", base))
.header("content-type", "application/x-flatbuffers")
.body(Vec::<u8>::new())
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 401);
assert_eq!(resp.headers().get("x-error-code").unwrap(), "UNAUTHORIZED");
assert_eq!(
resp.headers().get("x-error-message").unwrap(),
"missing or invalid bearer token"
);
assert!(resp.bytes().await.unwrap().is_empty());
let resp = client
.post(format!("{}/api/ping", base))
.header("content-type", "application/json")
.header("authorization", "Bearer secret")
.body(r#"{"message":"hi","value":1}"#)
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let body: TestResponse = resp.json().await.unwrap();
assert_eq!(body.value, 1 + 100);
server.abort();
}
#[test]
fn test_router_path_matching() {
use flatbed::hyper::Router;
let router = Router::new();
let methods = router.get_allowed_methods("/api/users");
assert!(methods.is_empty());
assert!(router.match_route("/api/ping", "GET").is_none());
}
#[tokio::test]
async fn accept_header_selects_the_response_codec() {
use flatbed::{Flatbed, FlatbedConfig};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let telemetry: Arc<dyn flatbed::TelemetryService> = Arc::new(StubTelemetryService);
let config = FlatbedConfig::new("Test API")
.host("127.0.0.1")
.port(port)
.with_telemetry(telemetry);
let server = tokio::spawn(async move { Flatbed::run(config, |_| async { Ok(()) }).await });
let base = format!("http://127.0.0.1:{port}");
let client = reqwest::Client::new();
for _ in 0..100 {
if let Ok(resp) = client.get(format!("{base}/readyz")).send().await {
if resp.status().as_u16() == 200 {
break;
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
let resp = client
.get(format!("{base}/api/resource"))
.header("accept", "application/json")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/json"
);
let json: flatbed::serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["code"], "BAD_REQUEST");
let resp = client
.get(format!("{base}/api/resource"))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/json"
);
let resp = client
.post(format!("{base}/api/error"))
.header("content-type", "application/json")
.header("accept", "application/x-flatbuffers")
.body(r#"{"message":"e","value":1}"#)
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/x-flatbuffers"
);
assert_eq!(resp.headers().get("x-error-code").unwrap(), "TEST_ERROR");
assert!(resp.bytes().await.unwrap().is_empty());
let request = TestRequest {
message: Some("fb".to_string()),
value: 1,
};
let resp = client
.post(format!("{base}/api/ping"))
.header("content-type", "application/x-flatbuffers")
.header("accept", "application/json")
.body(request.to_flatbuffer())
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/json"
);
let json: flatbed::serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["value"], 101);
assert_eq!(json["message"], "Test pong: fb");
let resp = client
.post(format!("{base}/api/ping"))
.header("content-type", "application/json")
.header("accept", "application/x-flatbuffers")
.body(r#"{"message":"j","value":2}"#)
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/x-flatbuffers"
);
let decoded = TestResponse::from_flatbuffer(&resp.bytes().await.unwrap()).unwrap();
assert_eq!(decoded.value, 102);
let resp = client
.post(format!("{base}/api/ping"))
.header("content-type", "application/json")
.header("accept", "text/html")
.body(r#"{"message":"h","value":3}"#)
.send()
.await
.unwrap();
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/json"
);
let resp = client
.post(format!("{base}/api/error"))
.header("content-type", "application/x-flatbuffers")
.header("accept", "application/json")
.body(
TestRequest {
message: Some("e".to_string()),
value: 1,
}
.to_flatbuffer(),
)
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/json"
);
let json: flatbed::serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["code"], "TEST_ERROR");
server.abort();
}