mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 12:42:28 +00:00
openapi spec tests
This commit is contained in:
parent
81803fd54a
commit
e5e9cd5d3c
7 changed files with 431 additions and 9 deletions
|
@ -19,7 +19,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
|||
|
||||
[dependencies]
|
||||
base64 = { version = "0.12.0", optional = true }
|
||||
chrono = { version = "0.4.11", optional = true }
|
||||
chrono = { version = "0.4.11", features = ["serde"], optional = true }
|
||||
cookie = { version = "0.13.3", optional = true }
|
||||
futures-core = "0.3.4"
|
||||
futures-util = "0.3.4"
|
||||
|
|
|
@ -3,10 +3,11 @@ use proc_macro2::{Ident, TokenStream};
|
|||
use quote::quote;
|
||||
use std::iter;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
DeriveInput,
|
||||
Error,
|
||||
Generics,
|
||||
Path,
|
||||
Result,
|
||||
|
@ -19,9 +20,7 @@ impl Parse for MimeList
|
|||
{
|
||||
fn parse(input: ParseStream) -> Result<Self>
|
||||
{
|
||||
let content;
|
||||
let _paren = parenthesized!(content in input);
|
||||
let list = Punctuated::parse_separated_nonempty(&content)?;
|
||||
let list = Punctuated::parse_separated_nonempty(&input)?;
|
||||
Ok(Self(list))
|
||||
}
|
||||
}
|
||||
|
@ -61,10 +60,15 @@ pub fn expand_request_body(input : DeriveInput) -> Result<TokenStream>
|
|||
|
||||
let types = input.attrs.into_iter()
|
||||
.filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()))
|
||||
.flat_map(|attr|
|
||||
syn::parse2::<MimeList>(attr.tokens)
|
||||
.flat_map(|attr| {
|
||||
let span = attr.span();
|
||||
attr.parse_args::<MimeList>()
|
||||
.map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>)
|
||||
.unwrap_or_else(|err| Box::new(iter::once(Err(err)))))
|
||||
.unwrap_or_else(|mut err| {
|
||||
err.combine(Error::new(span, "Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]"));
|
||||
Box::new(iter::once(Err(err)))
|
||||
})
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
let types = match types {
|
||||
|
|
202
tests/openapi_specification.json
Normal file
202
tests/openapi_specification.json
Normal file
|
@ -0,0 +1,202 @@
|
|||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Secret": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"format": "float",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
],
|
||||
"title": "Secret",
|
||||
"type": "object"
|
||||
},
|
||||
"Secrets": {
|
||||
"properties": {
|
||||
"secrets": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Secret"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secrets"
|
||||
],
|
||||
"title": "Secrets",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"authToken": {
|
||||
"bearerFormat": "JWT",
|
||||
"scheme": "bearer",
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"title": "This is just a test",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"openapi": "3.0.2",
|
||||
"paths": {
|
||||
"/img/{id}": {
|
||||
"get": {
|
||||
"operationId": "getImage",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"style": "simple"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"operationId": "setImage",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"style": "simple"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/secret/search": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "date",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "date",
|
||||
"type": "string"
|
||||
},
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "hour",
|
||||
"schema": {
|
||||
"format": "int16",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "minute",
|
||||
"schema": {
|
||||
"format": "int16",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"style": "form"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Secrets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"authToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/secret/{id}": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"style": "simple"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Secret"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"authToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:12345/api/v1"
|
||||
}
|
||||
]
|
||||
}
|
116
tests/openapi_specification.rs
Normal file
116
tests/openapi_specification.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))]
|
||||
|
||||
#[macro_use] extern crate gotham_derive;
|
||||
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use gotham::{
|
||||
pipeline::{new_pipeline, single::single_pipeline},
|
||||
router::builder::*,
|
||||
test::TestServer
|
||||
};
|
||||
use gotham_restful::*;
|
||||
use mime::IMAGE_PNG;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util { include!("util/mod.rs"); }
|
||||
use util::{test_get_response, test_openapi_response};
|
||||
|
||||
|
||||
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read, change)]
|
||||
struct ImageResource;
|
||||
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(IMAGE_PNG)]
|
||||
struct Image(Vec<u8>);
|
||||
|
||||
#[read(ImageResource, operation_id = "getImage")]
|
||||
fn get_image(_id : u64) -> Raw<&'static [u8]>
|
||||
{
|
||||
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
||||
}
|
||||
|
||||
#[change(ImageResource, operation_id = "setImage")]
|
||||
fn set_image(_id : u64, _image : Image)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read, search)]
|
||||
struct SecretResource;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct AuthData
|
||||
{
|
||||
sub : String,
|
||||
iat : u64,
|
||||
exp : u64
|
||||
}
|
||||
|
||||
type AuthStatus = gotham_restful::AuthStatus<AuthData>;
|
||||
|
||||
#[derive(OpenapiType, Serialize)]
|
||||
struct Secret
|
||||
{
|
||||
code : f32
|
||||
}
|
||||
|
||||
#[derive(OpenapiType, Serialize)]
|
||||
struct Secrets
|
||||
{
|
||||
secrets : Vec<Secret>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
|
||||
struct SecretQuery
|
||||
{
|
||||
date : NaiveDate,
|
||||
hour : Option<u16>,
|
||||
minute : Option<u16>
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
fn read_secret(auth : AuthStatus, _id : NaiveDateTime) -> AuthSuccess<Secret>
|
||||
{
|
||||
auth.ok()?;
|
||||
Ok(Secret { code: 4.2 })
|
||||
}
|
||||
|
||||
#[search(SecretResource)]
|
||||
fn search_secret(auth : AuthStatus, _query : SecretQuery) -> AuthSuccess<Secrets>
|
||||
{
|
||||
auth.ok()?;
|
||||
Ok(Secrets {
|
||||
secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn openapi_supports_scope()
|
||||
{
|
||||
let info = OpenapiInfo {
|
||||
title: "This is just a test".to_owned(),
|
||||
version: "1.2.3".to_owned(),
|
||||
urls: vec!["http://localhost:12345/api/v1".to_owned()]
|
||||
};
|
||||
let auth: AuthMiddleware<AuthData, _> = AuthMiddleware::new(
|
||||
AuthSource::AuthorizationHeader,
|
||||
AuthValidation::default(),
|
||||
StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc")
|
||||
);
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build());
|
||||
let server = TestServer::new(build_router(chain, pipelines, |router| {
|
||||
router.with_openapi(info, |mut router| {
|
||||
router.resource::<ImageResource>("img");
|
||||
router.get_openapi("openapi");
|
||||
router.resource::<SecretResource>("secret");
|
||||
});
|
||||
})).unwrap();
|
||||
|
||||
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json");
|
||||
}
|
78
tests/openapi_supports_scope.json
Normal file
78
tests/openapi_supports_scope.json
Normal file
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"components": {},
|
||||
"info": {
|
||||
"title": "Test",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"openapi": "3.0.2",
|
||||
"paths": {
|
||||
"/bar/baz/foo3": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bar/foo2": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/foo1": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/foo4": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ use mime::TEXT_PLAIN;
|
|||
|
||||
#[allow(dead_code)]
|
||||
mod util { include!("util/mod.rs"); }
|
||||
use util::test_get_response;
|
||||
use util::{test_get_response, test_openapi_response};
|
||||
|
||||
|
||||
const RESPONSE : &[u8] = b"This is the only valid response.";
|
||||
|
@ -34,6 +34,7 @@ fn openapi_supports_scope()
|
|||
};
|
||||
let server = TestServer::new(build_simple_router(|router| {
|
||||
router.with_openapi(info, |mut router| {
|
||||
router.get_openapi("openapi");
|
||||
router.resource::<FooResource>("foo1");
|
||||
router.scope("/bar", |router| {
|
||||
router.resource::<FooResource>("foo2");
|
||||
|
@ -49,4 +50,5 @@ fn openapi_supports_scope()
|
|||
test_get_response(&server, "http://localhost/bar/foo2", RESPONSE);
|
||||
test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE);
|
||||
test_get_response(&server, "http://localhost/foo4", RESPONSE);
|
||||
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_supports_scope.json");
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ use gotham::{
|
|||
test::TestServer
|
||||
};
|
||||
use mime::Mime;
|
||||
#[allow(unused_imports)]
|
||||
use std::{fs::File, io::{Read, Write}, str};
|
||||
|
||||
pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8])
|
||||
{
|
||||
|
@ -35,3 +37,21 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
|
|||
let body : &[u8] = res.as_ref();
|
||||
assert_eq!(body, expected);
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str)
|
||||
{
|
||||
let res = server.client().get(path).perform().unwrap().read_body().unwrap();
|
||||
let body = serde_json::to_string_pretty(&serde_json::from_slice::<serde_json::Value>(res.as_ref()).unwrap()).unwrap();
|
||||
match File::open(output_file) {
|
||||
Ok(mut file) => {
|
||||
let mut expected = String::new();
|
||||
file.read_to_string(&mut expected).unwrap();
|
||||
assert_eq!(body, expected);
|
||||
},
|
||||
Err(_) => {
|
||||
let mut file = File::create(output_file).unwrap();
|
||||
file.write_all(body.as_bytes()).unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue