1
0
Fork 0
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:
Dominic 2020-05-19 21:07:29 +02:00
parent 81803fd54a
commit e5e9cd5d3c
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
7 changed files with 431 additions and 9 deletions

View file

@ -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"

View file

@ -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 {

View 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"
}
]
}

View 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");
}

View 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"
}
}
}
}
}
}

View file

@ -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");
}

View file

@ -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();
}
};
}