Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
120
crates/adapters/http-axum/tests/log_review_form_test.rs
Normal file
120
crates/adapters/http-axum/tests/log_review_form_test.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use api_types::{DiaryQueryParams, LogReviewRequest};
|
||||
use http_axum::html::forms::{LogReviewData, LogReviewForm, to_diary_query};
|
||||
|
||||
fn make_form(watched_at: &str) -> LogReviewForm {
|
||||
LogReviewForm {
|
||||
external_metadata_id: None,
|
||||
manual_title: None,
|
||||
manual_release_year: None,
|
||||
manual_director: None,
|
||||
rating: 4,
|
||||
comment: None,
|
||||
watched_at: watched_at.to_string(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
csrf_token: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_request(watched_at: &str) -> LogReviewRequest {
|
||||
LogReviewRequest {
|
||||
external_metadata_id: None,
|
||||
manual_title: None,
|
||||
manual_release_year: None,
|
||||
manual_director: None,
|
||||
rating: 4,
|
||||
comment: None,
|
||||
watched_at: watched_at.to_string(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_accepts_datetime_with_seconds() {
|
||||
let data = LogReviewData::try_from(make_form("2024-03-15T20:30:00")).unwrap();
|
||||
assert_eq!(data.watched_at.format("%H:%M:%S").to_string(), "20:30:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_accepts_datetime_without_seconds() {
|
||||
let data = LogReviewData::try_from(make_form("2024-03-15T20:30")).unwrap();
|
||||
assert_eq!(data.watched_at.format("%H:%M").to_string(), "20:30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_rejects_invalid_datetime() {
|
||||
assert!(LogReviewData::try_from(make_form("not-a-date")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_accepts_datetime_with_seconds() {
|
||||
let data = LogReviewData::try_from(make_request("2024-03-15T20:30:00")).unwrap();
|
||||
assert_eq!(data.watched_at.format("%H:%M:%S").to_string(), "20:30:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_accepts_datetime_without_seconds() {
|
||||
let data = LogReviewData::try_from(make_request("2024-03-15T20:30")).unwrap();
|
||||
assert_eq!(data.watched_at.format("%H:%M").to_string(), "20:30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_rejects_invalid_datetime() {
|
||||
assert!(LogReviewData::try_from(make_request("garbage")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_external_id_becomes_none_in_form() {
|
||||
let mut form = make_form("2024-03-15T20:30:00");
|
||||
form.external_metadata_id = Some(" ".to_string());
|
||||
let data = LogReviewData::try_from(form).unwrap();
|
||||
assert!(data.external_metadata_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_external_id_becomes_none_in_request() {
|
||||
let mut req = make_request("2024-03-15T20:30:00");
|
||||
req.external_metadata_id = Some(" ".to_string());
|
||||
let data = LogReviewData::try_from(req).unwrap();
|
||||
assert!(data.external_metadata_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_by_asc_string_becomes_ascending() {
|
||||
let params = DiaryQueryParams {
|
||||
sort_by: Some("asc".to_string()),
|
||||
limit: None,
|
||||
offset: None,
|
||||
movie_id: None,
|
||||
user_id: None,
|
||||
};
|
||||
let query = to_diary_query(params);
|
||||
assert!(matches!(
|
||||
query.sort_by,
|
||||
Some(domain::models::ReviewSortBy::Ascending)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_by_other_string_becomes_descending() {
|
||||
let params = DiaryQueryParams {
|
||||
sort_by: Some("desc".to_string()),
|
||||
limit: None,
|
||||
offset: None,
|
||||
movie_id: None,
|
||||
user_id: None,
|
||||
};
|
||||
let query = to_diary_query(params);
|
||||
assert!(matches!(
|
||||
query.sort_by,
|
||||
Some(domain::models::ReviewSortBy::Descending)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_accepts_date_only() {
|
||||
let data = LogReviewData::try_from(make_form("2024-03-15")).unwrap();
|
||||
assert_eq!(data.watched_at.format("%H:%M:%S").to_string(), "00:00:00");
|
||||
assert_eq!(data.watched_at.format("%Y-%m-%d").to_string(), "2024-03-15");
|
||||
}
|
||||
94
crates/adapters/http-axum/tests/openapi_contract_test.rs
Normal file
94
crates/adapters/http-axum/tests/openapi_contract_test.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
fn spec() -> serde_json::Value {
|
||||
serde_json::to_value(http_axum::openapi::spec()).unwrap()
|
||||
}
|
||||
|
||||
fn operations(spec: &serde_json::Value) -> Vec<(String, String, serde_json::Value)> {
|
||||
spec["paths"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.flat_map(|(path, methods)| {
|
||||
methods
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(move |(verb, op)| (verb.to_uppercase(), path.clone(), op.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_spec_is_not_vacuous() {
|
||||
let spec = spec();
|
||||
assert!(!operations(&spec).is_empty());
|
||||
assert!(spec["components"]["schemas"]["ErrorResponse"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_2xx_json_response_declares_a_schema() {
|
||||
let spec = spec();
|
||||
let mut untyped = Vec::new();
|
||||
|
||||
for (verb, path, op) in operations(&spec) {
|
||||
for (code, r) in op["responses"].as_object().unwrap() {
|
||||
if !code.starts_with('2') {
|
||||
continue;
|
||||
}
|
||||
if let Some(json) = r["content"].get("application/json")
|
||||
&& !json["schema"].is_object()
|
||||
{
|
||||
untyped.push(format!("{verb} {path} -> {code}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(untyped.is_empty(), "2xx JSON without a schema: {untyped:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_documents_a_refusal() {
|
||||
let spec = spec();
|
||||
let silent: Vec<_> = operations(&spec)
|
||||
.into_iter()
|
||||
.filter(|(_, _, op)| {
|
||||
!op["responses"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.any(|c| c.starts_with('4'))
|
||||
})
|
||||
.map(|(v, p, _)| format!("{v} {p}"))
|
||||
.collect();
|
||||
assert!(silent.is_empty(), "no 4xx documented: {silent:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_authenticated_operation_documents_401() {
|
||||
let spec = spec();
|
||||
let silent: Vec<_> = operations(&spec)
|
||||
.into_iter()
|
||||
.filter(|(_, _, op)| op.get("security").is_some() && op["responses"].get("401").is_none())
|
||||
.map(|(v, p, _)| format!("{v} {p}"))
|
||||
.collect();
|
||||
assert!(silent.is_empty(), "no 401 documented: {silent:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_refusal_is_the_one_error_shape() {
|
||||
let spec = spec();
|
||||
let mut odd = Vec::new();
|
||||
for (verb, path, op) in operations(&spec) {
|
||||
for (code, response) in op["responses"].as_object().unwrap() {
|
||||
if !code.starts_with('4') && !code.starts_with('5') {
|
||||
continue;
|
||||
}
|
||||
let named = response["content"]["application/json"]["schema"]["$ref"]
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if named != "#/components/schemas/ErrorResponse" {
|
||||
odd.push(format!("{verb} {path} -> {code}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(odd.is_empty(), "refusals not ErrorResponse: {odd:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user