use poem::{ http::{Method, StatusCode}, test::TestClient, web::Data, Endpoint, EndpointExt, Error, }; use poem_openapi::{ param::{Path, Query}, payload::{Binary, Json, Payload, PlainText}, registry::{MetaApi, MetaExternalDocument, MetaOperation, MetaParamIn, MetaSchema, Registry}, types::Type, ApiRequest, ApiResponse, Object, OpenApi, OpenApiService, Tags, }; #[tokio::test] async fn path_and_method() { struct Api; #[OpenApi] impl Api { #[oai(path = "/abc", method = "post")] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert_eq!(meta.paths[0].path, "/abc"); assert_eq!(meta.paths[0].operations[0].method, Method::POST); let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); cli.post("/abc").send().await.assert_status_is_ok(); } #[test] fn deprecated() { struct Api; #[OpenApi] impl Api { #[oai(path = "/abc", method = "get", deprecated)] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert!(meta.paths[0].operations[0].deprecated); } #[test] fn tag() { #[derive(Tags)] enum MyTags { /// User operations UserOperations, /// Pet operations PetOperations, } struct Api; #[OpenApi] impl Api { #[oai( path = "/abc", method = "get", tag = "MyTags::UserOperations", tag = "MyTags::PetOperations" )] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert_eq!( meta.paths[0].operations[0].tags, vec!["UserOperations", "PetOperations"] ); } #[tokio::test] async fn common_tags() { #[derive(Tags)] enum MyTags { UserOperations, CommonOperations, } struct Api; #[OpenApi(prefix_path = "/hello", tag = "MyTags::CommonOperations")] impl Api { #[oai(path = "/world", method = "get", tag = "MyTags::UserOperations")] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert_eq!(meta.paths[0].path, "/hello/world"); assert_eq!( meta.paths[0].operations[0].tags, vec!["CommonOperations", "UserOperations"] ); } #[tokio::test] async fn prefix_path() { struct Api; #[OpenApi(prefix_path = "/hello")] impl Api { #[oai(path = "/world", method = "get")] async fn test(&self) {} #[oai(path = "/", method = "get")] async fn test1(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert_eq!(meta.paths[0].path, "/hello/world"); assert_eq!(meta.paths[1].path, "/hello"); let ep = OpenApiService::new(Api, "test", "1.0"); TestClient::new(ep) .get("/hello/world") .send() .await .assert_status_is_ok(); let ep = OpenApiService::new(Api, "test", "1.0"); TestClient::new(ep) .get("/hello") .send() .await .assert_status_is_ok(); const PREFIX: &str = "/hello2"; struct Api2; #[OpenApi(prefix_path = PREFIX)] impl Api2 { #[oai(path = "/world", method = "get")] async fn test(&self) {} #[oai(path = "/", method = "get")] async fn test1(&self) {} } let meta: MetaApi = Api2::meta().remove(0); assert_eq!(meta.paths[0].path, "/hello2/world"); assert_eq!(meta.paths[1].path, "/hello2"); let ep = OpenApiService::new(Api2, "test", "1.0"); TestClient::new(ep) .get("/hello2/world") .send() .await .assert_status_is_ok(); let ep = OpenApiService::new(Api2, "test", "1.0"); TestClient::new(ep) .get("/hello2") .send() .await .assert_status_is_ok(); struct Api3; #[OpenApi(prefix_path = "/hello3/")] impl Api3 { #[oai(path = "/world", method = "get")] async fn test(&self) {} #[oai(path = "/", method = "get")] async fn test1(&self) {} } let meta: MetaApi = Api3::meta().remove(0); assert_eq!(meta.paths[0].path, "/hello3/world"); assert_eq!(meta.paths[1].path, "/hello3/"); let ep = OpenApiService::new(Api3, "test", "1.0"); TestClient::new(ep) .get("/hello3/world") .send() .await .assert_status_is_ok(); let ep = OpenApiService::new(Api3, "test", "1.0"); TestClient::new(ep) .get("/hello3/") .send() .await .assert_status_is_ok(); struct Api4; #[OpenApi(prefix_path = "")] impl Api4 { #[oai(path = "/world", method = "get")] async fn test(&self) {} #[oai(path = "/", method = "get")] async fn test1(&self) {} } let meta: MetaApi = Api4::meta().remove(0); assert_eq!(meta.paths[0].path, "/world"); assert_eq!(meta.paths[1].path, "/"); let ep = OpenApiService::new(Api4, "test", "1.0"); TestClient::new(ep) .get("/world") .send() .await .assert_status_is_ok(); let ep = OpenApiService::new(Api4, "test", "1.0"); TestClient::new(ep) .get("/") .send() .await .assert_status_is_ok(); } #[tokio::test] async fn request() { /// Test request #[derive(ApiRequest)] enum MyRequest { Json(Json), PlainText(PlainText), Binary(Binary>), } struct Api; #[OpenApi] impl Api { #[oai(path = "/", method = "get")] async fn test(&self, req: MyRequest) { match req { MyRequest::Json(value) => { assert_eq!(value.0, 100); } MyRequest::PlainText(value) => { assert_eq!(value.0, "abc"); } MyRequest::Binary(value) => { assert_eq!(value.0, vec![1, 2, 3]); } } } } let meta: MetaApi = Api::meta().remove(0); let meta_request = meta.paths[0].operations[0].request.as_ref().unwrap(); assert!(meta_request.required); assert_eq!(meta_request.description, Some("Test request")); assert_eq!( meta_request.content[0].content_type, "application/json; charset=utf-8" ); assert_eq!(meta_request.content[0].schema, i32::schema_ref()); assert_eq!( meta_request.content[1].content_type, "text/plain; charset=utf-8" ); assert_eq!(meta_request.content[1].schema, String::schema_ref()); assert_eq!( meta_request.content[2].content_type, "application/octet-stream" ); assert_eq!( meta_request.content[2].schema.unwrap_inline(), &MetaSchema { format: Some("binary"), ..MetaSchema::new("string") } ); let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); cli.get("/") .content_type("application/json") .body("100") .send() .await .assert_status_is_ok(); cli.get("/") .content_type("application/json; x=10") .body("100") .send() .await .assert_status_is_ok(); cli.get("/") .content_type("text/plain") .body("abc") .send() .await .assert_status_is_ok(); cli.get("/") .content_type("application/octet-stream") .body(vec![1, 2, 3]) .send() .await .assert_status_is_ok(); } #[tokio::test] async fn payload_request() { struct Api; #[OpenApi] impl Api { #[oai(path = "/", method = "post")] async fn test(&self, req: Json) { assert_eq!(req.0, 100); } } let meta: MetaApi = Api::meta().remove(0); let meta_request = meta.paths[0].operations[0].request.as_ref().unwrap(); assert!(meta_request.required); assert_eq!( meta_request.content[0].content_type, "application/json; charset=utf-8" ); assert_eq!(meta_request.content[0].schema, i32::schema_ref()); let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); cli.post("/") .content_type("application/json") .body("100") .send() .await .assert_status_is_ok(); cli.post("/") .content_type("text/plain") .body("100") .send() .await .assert_status(StatusCode::UNSUPPORTED_MEDIA_TYPE); } #[tokio::test] async fn response() { #[derive(ApiResponse)] enum MyResponse { /// Ok #[oai(status = 200)] Ok, /// Already exists #[oai(status = 409)] AlreadyExists(Json), /// Default Default(StatusCode, PlainText), } struct Api; #[OpenApi] impl Api { #[oai(path = "/", method = "get")] async fn test(&self, code: Query) -> MyResponse { match code.0 { 200 => MyResponse::Ok, 409 => MyResponse::AlreadyExists(Json(code.0)), _ => MyResponse::Default( StatusCode::from_u16(code.0).unwrap(), PlainText(format!("code: {}", code.0)), ), } } } let meta: MetaApi = Api::meta().remove(0); let meta_responses = &meta.paths[0].operations[0].responses; assert_eq!(meta_responses.responses[0].description, "Ok"); assert_eq!(meta_responses.responses[0].status, Some(200)); assert!(meta_responses.responses[0].content.is_empty()); assert_eq!(meta_responses.responses[1].description, "Already exists"); assert_eq!(meta_responses.responses[1].status, Some(409)); assert_eq!( meta_responses.responses[1].content[0].content_type, "application/json; charset=utf-8" ); assert_eq!( meta_responses.responses[1].content[0].schema, u16::schema_ref() ); assert_eq!(meta_responses.responses[2].description, "Default"); assert_eq!(meta_responses.responses[2].status, None); assert_eq!( meta_responses.responses[2].content[0].content_type, "text/plain; charset=utf-8" ); assert_eq!( meta_responses.responses[2].content[0].schema, String::schema_ref() ); let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); let resp = cli.get("/").query("code", &200).send().await; resp.assert_status_is_ok(); resp.assert_text("").await; let resp = cli.get("/").query("code", &409).send().await; resp.assert_status(StatusCode::CONFLICT); resp.assert_content_type("application/json; charset=utf-8"); resp.assert_text("409").await; let resp = cli.get("/").query("code", &404).send().await; resp.assert_status(StatusCode::NOT_FOUND); resp.assert_content_type("text/plain; charset=utf-8"); resp.assert_text("code: 404").await; } #[tokio::test] async fn bad_request_handler() { #[derive(ApiResponse)] #[oai(bad_request_handler = "bad_request_handler")] enum MyResponse { /// Ok #[oai(status = 200)] Ok(PlainText), /// Already exists #[oai(status = 400)] BadRequest(PlainText), } fn bad_request_handler(err: Error) -> MyResponse { MyResponse::BadRequest(PlainText(format!("!!! {err}"))) } struct Api; #[OpenApi] impl Api { #[oai(path = "/", method = "get")] async fn test(&self, code: Query) -> MyResponse { MyResponse::Ok(PlainText(format!("code: {}", code.0))) } } let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); let resp = cli.get("/").query("code", &200).send().await; resp.assert_status_is_ok(); resp.assert_content_type("text/plain; charset=utf-8"); resp.assert_text("code: 200").await; let resp = cli.get("/").send().await; resp.assert_status(StatusCode::BAD_REQUEST); resp.assert_content_type("text/plain; charset=utf-8"); resp.assert_text( r#"!!! failed to parse parameter `code`: Type "integer(uint16)" expects an input value."#, ) .await; } #[tokio::test] async fn bad_request_handler_for_validator() { #[derive(ApiResponse)] #[oai(bad_request_handler = "bad_request_handler")] enum MyResponse { /// Ok #[oai(status = 200)] Ok(PlainText), /// Already exists #[oai(status = 400)] BadRequest(PlainText), } fn bad_request_handler(err: Error) -> MyResponse { MyResponse::BadRequest(PlainText(format!("!!! {err}"))) } struct Api; #[OpenApi] impl Api { #[oai(path = "/", method = "get")] async fn test( &self, #[oai(validator(maximum(value = "100")))] code: Query, ) -> MyResponse { MyResponse::Ok(PlainText(format!("code: {}", code.0))) } } let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); let resp = cli.get("/").query("code", &50).send().await; resp.assert_status_is_ok(); resp.assert_content_type("text/plain; charset=utf-8"); resp.assert_text("code: 50").await; let resp = cli.get("/").query("code", &200).send().await; resp.assert_status(StatusCode::BAD_REQUEST); resp.assert_content_type("text/plain; charset=utf-8"); resp.assert_text(r#"!!! failed to parse parameter `code`: verification failed. maximum(100, exclusive: false)"#).await; } #[tokio::test] async fn poem_extract() { struct Api; #[OpenApi] impl Api { #[oai(path = "/test1", method = "get")] async fn test1(&self, data: Data<&i32>) { assert_eq!(*data.0, 100); } #[oai(path = "/test2", method = "get")] async fn test2(&self, Data(value): Data<&i32>) { assert_eq!(*value, 100); } #[oai(path = "/test3/:user_id", method = "get")] async fn test3(&self, Path(user_id): Path) { assert_eq!(user_id, 7); } } let ep = OpenApiService::new(Api, "test", "1.0").data(100i32); let client = TestClient::new(ep); client.get("/test1").send().await.assert_status_is_ok(); client.get("/test2").send().await.assert_status_is_ok(); client.get("/test3/7").send().await.assert_status_is_ok(); } #[tokio::test] async fn returning_borrowed_value() { struct Api { value1: i32, value2: String, values: Vec, } #[OpenApi] impl Api { #[oai(path = "/value1", method = "get")] async fn value1(&self) -> Json<&i32> { Json(&self.value1) } #[oai(path = "/value2", method = "get")] async fn value2(&self) -> Json<&String> { Json(&self.value2) } #[oai(path = "/value3", method = "get")] async fn value3<'a>(&self, data: Data<&'a i32>) -> Json<&'a i32> { Json(&data) } #[oai(path = "/value4", method = "get")] async fn value4<'a>(&self, Data(value): Data<&'a i32>) -> Json<&'a i32> { Json(value) } #[oai(path = "/values", method = "get")] async fn values(&self) -> Json<&[i32]> { Json(&self.values) } } let ep = OpenApiService::new( Api { value1: 999, value2: "abc".to_string(), values: vec![1, 2, 3, 4, 5], }, "test", "1.0", ) .data(888i32); let cli = TestClient::new(ep); let resp = cli.get("/value1").send().await; resp.assert_status_is_ok(); resp.assert_text("999").await; let resp = cli.get("/value2").send().await; resp.assert_status_is_ok(); resp.assert_text("\"abc\"").await; let resp = cli.get("/value3").send().await; resp.assert_status_is_ok(); resp.assert_text("888").await; let resp = cli.get("/value4").send().await; resp.assert_status_is_ok(); resp.assert_text("888").await; let resp = cli.get("/values").send().await; resp.assert_status_is_ok(); resp.assert_text("[1,2,3,4,5]").await; } #[tokio::test] async fn external_docs() { struct Api; #[OpenApi] impl Api { #[oai( path = "/", method = "get", external_docs = "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md" )] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert_eq!( meta.paths[0].operations[0].external_docs, Some(MetaExternalDocument { url: "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md" .to_string(), description: None }) ); } #[tokio::test] async fn generic() { trait MyApiPort: Send + Sync + 'static { fn test(&self) -> String; } struct MyApiA; impl MyApiPort for MyApiA { fn test(&self) -> String { "test".to_string() } } struct MyOpenApi { api: MyApi, } #[OpenApi] impl MyOpenApi { #[oai(path = "/some_call", method = "get")] async fn some_call(&self) -> Json { Json(self.api.test()) } } let ep = OpenApiService::new(MyOpenApi { api: MyApiA }, "test", "1.0"); let cli = TestClient::new(ep); let resp = cli.get("/some_call").send().await; resp.assert_status_is_ok(); resp.assert_json("test").await; } #[tokio::test] async fn extra_response_headers_on_operation() { struct Api; #[OpenApi] impl Api { #[oai( path = "/", method = "get", response_header(name = "A1", ty = "String", description = "abc"), response_header(name = "a2", ty = "i32", deprecated = true) )] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); let header = &meta.paths[0].operations[0].responses.responses[0].headers[0]; assert_eq!(header.name, "A1"); assert_eq!(header.description.as_deref(), Some("abc")); assert!(!header.deprecated); assert_eq!(header.schema, String::schema_ref()); let header = &meta.paths[0].operations[0].responses.responses[0].headers[1]; assert_eq!(header.name, "A2"); assert_eq!(header.description, None); assert!(header.deprecated); assert_eq!(header.schema, i32::schema_ref()); } #[tokio::test] async fn extra_response_headers_on_api() { struct Api; #[OpenApi( response_header(name = "A1", ty = "String", description = "abc"), response_header(name = "a2", ty = "i32", deprecated = true) )] impl Api { #[oai(path = "/", method = "get")] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); let header = &meta.paths[0].operations[0].responses.responses[0].headers[0]; assert_eq!(header.name, "A1"); assert_eq!(header.description.as_deref(), Some("abc")); assert!(!header.deprecated); assert_eq!(header.schema, String::schema_ref()); let header = &meta.paths[0].operations[0].responses.responses[0].headers[1]; assert_eq!(header.name, "A2"); assert_eq!(header.description, None); assert!(header.deprecated); assert_eq!(header.schema, i32::schema_ref()); } #[tokio::test] async fn extra_request_headers_on_operation() { struct Api; #[OpenApi] impl Api { #[oai( path = "/", method = "get", request_header(name = "A1", ty = "String", description = "abc"), request_header(name = "a2", ty = "i32", deprecated = true) )] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); let params = &meta.paths[0].operations[0].params[0]; assert_eq!(params.name, "A1"); assert_eq!(params.schema, String::schema_ref()); assert_eq!(params.in_type, MetaParamIn::Header); assert_eq!(params.description.as_deref(), Some("abc")); assert!(params.required); assert!(!params.deprecated); let params = &meta.paths[0].operations[0].params[1]; assert_eq!(params.name, "A2"); assert_eq!(params.schema, i32::schema_ref()); assert_eq!(params.in_type, MetaParamIn::Header); assert_eq!(params.description, None); assert!(params.required); assert!(params.deprecated); } #[tokio::test] async fn extra_request_headers_on_api() { struct Api; #[OpenApi( request_header(name = "A1", ty = "String", description = "abc"), request_header(name = "a2", ty = "i32", deprecated = true) )] impl Api { #[oai(path = "/", method = "get")] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); let params = &meta.paths[0].operations[0].params[0]; assert_eq!(params.name, "A1"); assert_eq!(params.schema, String::schema_ref()); assert_eq!(params.in_type, MetaParamIn::Header); assert_eq!(params.description.as_deref(), Some("abc")); assert!(params.required); assert!(!params.deprecated); let params = &meta.paths[0].operations[0].params[1]; assert_eq!(params.name, "A2"); assert_eq!(params.schema, i32::schema_ref()); assert_eq!(params.in_type, MetaParamIn::Header); assert_eq!(params.description, None); assert!(params.required); assert!(params.deprecated); } #[tokio::test] async fn multiple_methods() { struct Api; #[OpenApi] impl Api { #[oai(path = "/abc", method = "post", method = "put")] async fn test(&self) {} } let meta: MetaApi = Api::meta().remove(0); assert_eq!(meta.paths[0].path, "/abc"); assert_eq!(meta.paths[0].operations[0].method, Method::POST); assert_eq!(meta.paths[0].operations[1].method, Method::PUT); let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); cli.post("/abc").send().await.assert_status_is_ok(); cli.put("/abc").send().await.assert_status_is_ok(); } #[tokio::test] async fn actual_type() { #[derive(Debug, Object)] struct MyObj { value: i32, } struct Api; #[OpenApi] impl Api { #[oai(path = "/", method = "get", actual_type = "Json")] async fn test(&self) -> Binary> { Binary(b"{ \"value\": 100 }".to_vec()) } } let meta: MetaApi = Api::meta().remove(0); assert_eq!(meta.paths[0].path, "/"); let operator = &meta.paths[0].operations[0]; let response = &operator.responses.responses[0]; assert_eq!(response.status, Some(200)); let media = &response.content[0]; assert_eq!(media.content_type, "application/json; charset=utf-8"); assert_eq!(media.schema, >::schema_ref()); let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); let resp = cli.get("/").send().await; resp.assert_content_type("application/json; charset=utf-8"); resp.assert_json(&serde_json::json!({ "value": 100 })).await; let mut registry = Registry::new(); Api::register(&mut registry); let type_name: Vec<&String> = registry.schemas.keys().collect(); assert_eq!(&type_name, &["MyObj"]); } #[tokio::test] async fn code_samples() { const JS_SOURCE: &str = " J S JavaScript "; struct Api; #[OpenApi] impl Api { #[oai( path = "/test1", method = "get", code_sample(lang = "js", source = "JS_SOURCE") )] async fn test1(&self) {} #[oai( path = "/test2", method = "get", code_sample(lang = "rust", label = "Rust lang", source = "\"Rust Source\""), code_sample(lang = "go", label = "Go", source = "\"Google Go\"") )] async fn test2(&self) {} } let meta: MetaApi = Api::meta().remove(0); let path = &meta.paths[0]; let operator: &MetaOperation = &path.operations[0]; assert_eq!(path.path, "/test1"); let code_sample = &operator.code_samples[0]; assert_eq!(code_sample.lang, "js"); assert_eq!(code_sample.label, None); assert_eq!(code_sample.source, JS_SOURCE); let path = &meta.paths[1]; let operator: &MetaOperation = &path.operations[0]; assert_eq!(path.path, "/test2"); let code_sample = &operator.code_samples[0]; assert_eq!(code_sample.lang, "rust"); assert_eq!(code_sample.label, Some("Rust lang")); assert_eq!(code_sample.source, "Rust Source"); let code_sample = &operator.code_samples[1]; assert_eq!(code_sample.lang, "go"); assert_eq!(code_sample.label, Some("Go")); assert_eq!(code_sample.source, "Google Go"); } #[tokio::test] async fn hidden() { #[derive(Debug, Object)] struct MyObj1 { value: i32, } #[derive(Debug, Object)] struct MyObj2 { value: i32, } struct Api; #[OpenApi] impl Api { #[oai(path = "/api1", method = "get", hidden)] async fn api1(&self, req: Json) -> Json { Json(req.0.value) } #[oai(path = "/api2", method = "get")] async fn api2(&self, req: Json) -> Json { Json(req.0.value) } } let meta: MetaApi = Api::meta().remove(0); assert_eq!(meta.paths.len(), 1); let path = &meta.paths[0]; assert_eq!(path.path, "/api2"); let mut registry = Registry::new(); Api::register(&mut registry); assert!(!registry.schemas.contains_key("MyObj1")); assert!(registry.schemas.contains_key("MyObj2")); } #[test] fn issue_405() { #[allow(dead_code)] struct Api; #[OpenApi] impl Api { #[oai( path = "/hello", method = "get", operation_id = "hello", transform = "my_transformer" )] #[allow(dead_code)] async fn index(&self) -> PlainText { PlainText("hello, world!".to_string()) } } #[allow(dead_code)] fn my_transformer(ep: impl Endpoint) -> impl Endpoint { ep.map_to_response() } } #[tokio::test] async fn issue_489() { struct Api; #[OpenApi] impl Api { #[oai(path = "/hello", method = "get")] async fn get_hello(&self) {} #[oai(path = "/hello", method = "delete")] async fn delete_hello(&self) {} #[oai(path = "/goodbye", method = "get")] async fn get_goodbye(&self) {} } let ep = OpenApiService::new(Api, "test", "1.0"); let cli = TestClient::new(ep); cli.delete("/goodbye") .send() .await .assert_status(StatusCode::METHOD_NOT_ALLOWED); }