use autumn_web::prelude::*;
use autumn_web::test::{TestApp, TestClient};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default)]
struct Filter {
status: String,
limit: Option<u32>,
}
#[derive(Serialize, Deserialize)]
struct Item {
sku: String,
qty: u32,
}
#[derive(Serialize, Deserialize, Default)]
struct SearchArgs {
q: String,
page: Option<u32>,
tags: Option<Vec<String>>,
filter: Option<Filter>,
items: Option<Vec<Item>>,
}
#[get("/search")]
async fn search(Query(args): Query<SearchArgs>) -> AutumnResult<Json<SearchArgs>> {
Ok(Json(args))
}
fn client() -> TestClient {
TestApp::new().routes(routes![search]).build()
}
#[tokio::test]
async fn flat_scalars_still_decode() {
let resp = client().get("/search?q=foo&page=2").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["q"], "foo");
assert_eq!(out["page"], 2);
assert!(out["tags"].is_null(), "absent field stays None: {out}");
}
#[tokio::test]
async fn absent_optionals_are_none() {
let resp = client().get("/search?q=").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["q"], "");
assert!(out["page"].is_null());
assert!(out["filter"].is_null());
}
#[tokio::test]
async fn repeated_keys_decode_into_a_sequence() {
let resp = client().get("/search?q=x&tags=a&tags=b").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["tags"], serde_json::json!(["a", "b"]));
}
#[tokio::test]
async fn single_occurrence_decodes_into_a_one_element_sequence() {
let resp = client().get("/search?q=x&tags=only").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["tags"], serde_json::json!(["only"]));
}
#[tokio::test]
async fn empty_bracket_append_form_decodes_into_a_sequence() {
let resp = client().get("/search?q=x&tags[]=a&tags[]=b").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["tags"], serde_json::json!(["a", "b"]));
}
#[tokio::test]
async fn indexed_form_decodes_in_ascending_index_order() {
let resp = client().get("/search?q=x&tags[2]=c&tags[0]=a").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["tags"], serde_json::json!(["a", "c"]));
}
#[tokio::test]
async fn nested_object_field_decodes() {
let resp = client()
.get("/search?q=x&filter[status]=open&filter[limit]=5")
.send()
.await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["filter"]["status"], "open");
assert_eq!(out["filter"]["limit"], 5);
}
#[tokio::test]
async fn array_of_objects_decodes() {
let resp = client()
.get("/search?q=x&items[0][sku]=A-1&items[0][qty]=2&items[1][sku]=B-2&items[1][qty]=3")
.send()
.await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["items"][0]["sku"], "A-1");
assert_eq!(out["items"][0]["qty"], 2);
assert_eq!(out["items"][1]["sku"], "B-2");
assert_eq!(out["items"][1]["qty"], 3);
}
#[tokio::test]
async fn percent_encoded_brackets_are_equivalent() {
let resp = client()
.get("/search?q=x&filter%5Bstatus%5D=open")
.send()
.await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["filter"]["status"], "open");
}
#[tokio::test]
async fn malformed_bracket_keys_stay_literal_and_are_ignored() {
let resp = client().get("/search?q=x&weird[unclosed=1").send().await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["q"], "x");
}
#[tokio::test]
async fn scalar_coercion_failure_is_a_problem_details_400() {
let resp = client().get("/search?q=x&page=not-a-number").send().await;
resp.assert_status(400);
resp.assert_header_contains("content-type", "application/problem+json");
let out: serde_json::Value = resp.json();
assert_eq!(out["code"], "autumn.bad_request");
}
#[tokio::test]
async fn conflicting_shapes_for_one_key_are_rejected() {
let resp = client()
.get("/search?q=x&filter=flat&filter[status]=open")
.send()
.await;
resp.assert_status(400);
}
#[tokio::test]
async fn excessive_nesting_depth_is_rejected() {
let deep = "filter".to_owned() + &"[x]".repeat(64);
let resp = client().get(&format!("/search?q=x&{deep}=1")).send().await;
resp.assert_status(400);
}
#[tokio::test]
async fn unresolvable_keys_the_handler_ignores_do_not_fail_the_request() {
let resp = client()
.get("/search?q=x&utm=1&utm[source]=news&junk[a][b][c][d][e][f][g][h][i][j][k][l][m][n][o][p][q]=1")
.send()
.await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["q"], "x");
}
#[tokio::test]
async fn a_duplicated_scalar_parameter_is_rejected() {
let resp = client().get("/search?q=first&q=second").send().await;
resp.assert_status(400);
}
#[tokio::test]
async fn a_failed_coercion_does_not_echo_the_value() {
let resp = client().get("/search?q=x&page=SUPERSECRET").send().await;
resp.assert_status(400);
let body: serde_json::Value = resp.json();
let rendered = body.to_string();
assert!(
!rendered.contains("SUPERSECRET"),
"value must not be reflected: {rendered}"
);
assert!(
rendered.contains("page"),
"the field path is still named: {rendered}"
);
}
#[tokio::test]
async fn huge_indices_do_not_preallocate() {
let resp = client()
.get("/search?q=x&tags[4000000000]=late&tags[1]=early")
.send()
.await;
resp.assert_ok();
let out: serde_json::Value = resp.json();
assert_eq!(out["tags"], serde_json::json!(["early", "late"]));
}