Skip to content

Commit c09bb49

Browse files
committed
add response status validation
1 parent 84d649d commit c09bb49

29 files changed

+429
-92
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
target
22
Cargo.lock
33
*.bk
4+
.env

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ serde_json = "1"
2323
serde_yaml = "0.8"
2424
url = "1.7"
2525
url_serde = "0.2"
26+
reqwest = "0.9"
2627

2728
[dev-dependencies]
29+
dotenv = "0.14"
30+
colored = "1"
2831
maplit = "1"
2932
pretty_assertions = "0.6"
3033
pretty_env_logger = "0.3"

examples/conformance.rs

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#![feature(todo_macro)]
2+
#![allow(dead_code, unused_imports, unused_variables)]
3+
4+
use std::env;
5+
6+
use colored::Colorize;
7+
use http::{Method, StatusCode};
8+
use log::{debug, info};
9+
use oas3::{
10+
conformance::{
11+
ConformanceTestSpec, OperationSpec, RequestSpec, ResolvedConformanceTestSpec, ResponseSpec,
12+
TestAuthorization, TestRequest,
13+
},
14+
validation::Error as ValidationError,
15+
Error, Spec,
16+
};
17+
18+
fn do_request(req: &TestRequest) -> Result<reqwest::Response, reqwest::Error> {
19+
let base_url = "http://localhost:9000/api/auth/v1";
20+
let client = reqwest::Client::new();
21+
22+
// TODO: add url params
23+
// TODO: add qs params
24+
25+
let method: reqwest::Method = req.operation.method.as_str().parse().unwrap();
26+
let url: String = [base_url, &req.operation.path].concat();
27+
28+
client
29+
.request(method, &url)
30+
.headers(req.headers.clone())
31+
.body(req.body.to_vec())
32+
.send()
33+
}
34+
35+
fn do_test(spec: &Spec, test: ResolvedConformanceTestSpec) -> Result<(), ValidationError> {
36+
debug!("request: {:?}", &test.request);
37+
debug!("response spec: {:?}", &test.response);
38+
39+
let mut res = do_request(&test.request).unwrap();
40+
let body = res.json().map_err(|_| ValidationError::NotJson)?;
41+
let status = res.status();
42+
43+
debug!("response: {:?}", &res);
44+
45+
let validation = test.response.validate_status(&res.status())?;
46+
info!("validation: {:?}", &validation);
47+
48+
let validation = test.response.validate_body(&body)?;
49+
info!("validation: {:?}", &validation);
50+
51+
Ok(())
52+
}
53+
54+
fn do_tests(
55+
spec: &Spec,
56+
tests: &[&ConformanceTestSpec],
57+
) -> Vec<(ConformanceTestSpec, Option<Error>)> {
58+
let mut results = vec![];
59+
60+
for test in tests {
61+
match test.resolve(&spec) {
62+
Ok(resolved_test) => {
63+
let validation = do_test(&spec, resolved_test);
64+
results.push(((*test).clone(), validation.map_err(Error::Validation).err()));
65+
}
66+
Err(err) => results.push(((*test).clone(), Some(err))),
67+
}
68+
}
69+
70+
results
71+
}
72+
73+
fn main() {
74+
let _ = dotenv::dotenv();
75+
pretty_env_logger::init();
76+
77+
let spec = oas3::from_path("../app/docs/api.yml")
78+
.expect("api spec parse error");
79+
80+
let auth_method = TestAuthorization::bearer(env::var("TOKEN").unwrap());
81+
82+
let test_pass0 = ConformanceTestSpec::new(
83+
OperationSpec::post("/token"),
84+
RequestSpec::from_example("application/json", "basic"),
85+
ResponseSpec::from_schema("200", "application/json"),
86+
);
87+
88+
let test_pass1 = ConformanceTestSpec::new(
89+
OperationSpec::post("/verify"),
90+
RequestSpec::from_json_example("revoked"),
91+
ResponseSpec::from_example("200", "application/json", "revoked"),
92+
);
93+
94+
let test_fail0 = ConformanceTestSpec::new(
95+
OperationSpec::operation_id("signin"),
96+
RequestSpec::from_json_example("unregistered"),
97+
ResponseSpec::from_example("401", "application/json", "success"),
98+
);
99+
100+
let test_fail1 = ConformanceTestSpec::new(
101+
OperationSpec::operation_id("checkLoggedIn"),
102+
RequestSpec::empty().with_auth(&auth_method),
103+
ResponseSpec::from_status("200"),
104+
);
105+
106+
let results = do_tests(&spec, &[&test_pass0, &test_pass1, &test_fail0, &test_fail1]);
107+
108+
println!("");
109+
print_test_results(results.as_slice());
110+
println!("");
111+
}
112+
113+
fn print_test_results(results: &[(ConformanceTestSpec, Option<Error>)]) {
114+
for (test, error) in results {
115+
let mut msg = vec![];
116+
117+
let op = &test.operation;
118+
119+
if error.is_some() {
120+
msg.push("❌ Err".red());
121+
} else {
122+
msg.push("✅ Ok ".green());
123+
}
124+
125+
msg.push(" | ".normal());
126+
msg.push(format!("{}", &op).normal());
127+
128+
if let Some(ref err) = error {
129+
msg.push(" | ".normal());
130+
msg.push(err.to_string().red());
131+
}
132+
133+
for part in msg {
134+
print!("{}", part);
135+
}
136+
137+
println!("");
138+
}
139+
}

src/components.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::collections::BTreeMap;
22

33
use crate::{
4-
Callback, Example, Header, Link, Parameter, RequestBody, Response, Schema,
5-
SecurityScheme, ObjectOrReference,
4+
Callback, Example, Header, Link, ObjectOrReference, Parameter, RequestBody, Response, Schema,
5+
SecurityScheme,
66
};
77

88
/// Holds a set of reusable objects for different aspects of the OAS.

src/conformance/auth.rs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use http::{Method, StatusCode};
2+
3+
use crate::{validation::Error as ValidationError, Operation, Spec};
4+
5+
6+
#[derive(Debug, Clone)]
7+
pub enum TestAuthorization {
8+
Bearer(String)
9+
}
10+
11+
impl TestAuthorization {
12+
pub fn bearer<T: Into<String>>(token: T) -> Self {
13+
Self::Bearer(token.into())
14+
}
15+
}

src/conformance/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
mod auth;
12
mod error;
23
mod operation;
34
mod request;
45
mod response;
56
mod test;
67

8+
pub use auth::*;
79
pub use error::*;
810
pub use operation::*;
911
pub use request::*;

src/conformance/operation.rs

+40-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
use std::fmt;
2+
13
use http::{Method, StatusCode};
24

5+
use crate::{validation::Error as ValidationError, Operation, Spec};
6+
37
#[derive(Debug, Clone)]
4-
pub struct OperationSpec {
5-
pub method: Method,
6-
pub path: String,
8+
pub enum OperationSpec {
9+
Parts { method: Method, path: String },
10+
OperationId(String),
711
}
812

913
impl OperationSpec {
1014
pub fn new<P: Into<String>>(method: Method, path: P) -> Self {
11-
Self {
15+
Self::Parts {
1216
method,
1317
path: path.into(),
1418
}
@@ -19,4 +23,36 @@ impl OperationSpec {
1923
pub fn patch<P: Into<String>>(path: P) -> Self { Self::new(Method::PATCH, path) }
2024
pub fn put<P: Into<String>>(path: P) -> Self { Self::new(Method::PUT, path) }
2125
pub fn delete<P: Into<String>>(path: P) -> Self { Self::new(Method::DELETE, path) }
26+
27+
pub fn operation_id<P: Into<String>>(op_id: P) -> Self { Self::OperationId(op_id.into()) }
28+
}
29+
30+
impl fmt::Display for OperationSpec {
31+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32+
match self {
33+
Self::Parts { method, path } => write!(f, "{} {}", method, path),
34+
Self::OperationId(op_id) => write!(f, "Operation ID: {}", op_id)
35+
}
36+
}
37+
}
38+
39+
#[derive(Debug, Clone)]
40+
pub struct TestOperation {
41+
pub method: Method,
42+
pub path: String,
43+
}
44+
45+
impl TestOperation {
46+
pub fn new<P: Into<String>>(method: Method, path: P) -> Self {
47+
Self {
48+
method,
49+
path: path.into(),
50+
}
51+
}
52+
53+
pub fn resolve_operation<'a>(&self, spec: &'a Spec) -> Result<&'a Operation, ValidationError> {
54+
spec.get_operation(&self.method, &self.path).ok_or_else(|| {
55+
ValidationError::OperationNotFound(self.method.clone(), self.path.clone())
56+
})
57+
}
2258
}

src/conformance/request.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
use bytes::Bytes;
22
use http::HeaderMap;
33

4-
use super::OperationSpec;
4+
use super::{OperationSpec, TestAuthorization, TestOperation};
55

66
#[derive(Debug, Clone)]
77
pub enum RequestSource {
88
Example { media_type: String, name: String },
99
Raw(Bytes),
10+
Empty,
1011
}
1112

1213
#[derive(Debug, Clone)]
1314
pub struct RequestSpec {
1415
pub source: RequestSource,
1516
pub bad: bool,
17+
pub auth: Option<TestAuthorization>,
1618
}
1719

1820
impl RequestSpec {
21+
pub fn empty() -> Self {
22+
Self {
23+
source: RequestSource::Empty,
24+
bad: false,
25+
auth: None,
26+
}
27+
}
28+
1929
pub fn from_example<M, N>(media_type: M, name: N) -> Self
2030
where
2131
M: Into<String>,
@@ -27,6 +37,7 @@ impl RequestSpec {
2737
name: name.into(),
2838
},
2939
bad: false,
40+
auth: None,
3041
}
3142
}
3243

@@ -40,6 +51,7 @@ impl RequestSpec {
4051
name: name.into(),
4152
},
4253
bad: false,
54+
auth: None,
4355
}
4456
}
4557

@@ -50,14 +62,21 @@ impl RequestSpec {
5062
Self {
5163
source: RequestSource::Raw(body.into()),
5264
bad: true,
65+
auth: None,
5366
}
5467
}
55-
}
5668

69+
pub fn with_auth(self, auth: &TestAuthorization) -> Self {
70+
Self {
71+
auth: Some(auth.clone()),
72+
..self
73+
}
74+
}
75+
}
5776

5877
#[derive(Debug, Clone)]
5978
pub struct TestRequest {
60-
pub operation: OperationSpec,
79+
pub operation: TestOperation,
6180
pub headers: HeaderMap,
6281
// pub parameters: Vec<_>,
6382
pub body: Bytes,

src/conformance/response.rs

+32-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use bytes::Bytes;
22
use http::StatusCode;
3+
use serde_json::Value as JsonValue;
34

4-
use super::OperationSpec;
5+
use super::{OperationSpec, TestOperation};
56
use crate::validation::{Error as ValidationError, SchemaValidator};
67

78
#[derive(Debug, Clone)]
89
pub enum ResponseSpecSource {
10+
Status(StatusCode),
911
Schema {
1012
status: StatusCode,
1113
media_type: String,
@@ -24,6 +26,12 @@ pub struct ResponseSpec {
2426
}
2527

2628
impl ResponseSpec {
29+
pub fn from_status(status: &str) -> Self {
30+
Self {
31+
source: ResponseSpecSource::Status(status.parse().unwrap()),
32+
}
33+
}
34+
2735
pub fn from_schema<M>(status: &str, media_type: M) -> Self
2836
where
2937
M: Into<String>,
@@ -53,14 +61,31 @@ impl ResponseSpec {
5361

5462
#[derive(Debug, Clone)]
5563
pub struct TestResponseSpec {
56-
pub operation: OperationSpec,
57-
pub body_validator: SchemaValidator,
64+
pub operation: TestOperation,
65+
pub status: StatusCode,
66+
pub body_validator: Option<SchemaValidator>,
5867
}
5968

6069
impl TestResponseSpec {
61-
pub fn validate(&self, val: &serde_json::Value) -> Result<(), ValidationError> {
62-
self.body_validator
63-
.validate_type(val)
64-
.and(self.body_validator.validate_required_fields(val))
70+
// TODO: own response type
71+
72+
pub fn validate_status(&self, val: &StatusCode) -> Result<(), ValidationError> {
73+
if &self.status == val {
74+
Ok(())
75+
} else {
76+
Err(ValidationError::StatusMismatch(
77+
self.status.clone(),
78+
val.clone(),
79+
))
80+
}
81+
}
82+
83+
pub fn validate_body(&self, body: &JsonValue) -> Result<(), ValidationError> {
84+
if let Some(ref vltr) = self.body_validator {
85+
vltr.validate_type(body)?;
86+
vltr.validate_required_fields(body)?;
87+
}
88+
89+
Ok(())
6590
}
6691
}

0 commit comments

Comments
 (0)