Keep the REST contract; move domain logic out of route files so checkout, fulfillment, and identity can be reused across customer, shop, and admin surfaces. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
6.3 KiB
Rust
208 lines
6.3 KiB
Rust
mod common;
|
|
|
|
use common::{client, register_customer, spawn_app};
|
|
use serial_test::serial;
|
|
|
|
fn addr_body(recipient: &str, city: &str, is_default: bool) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"recipient": recipient,
|
|
"phone": "+1 555 0100",
|
|
"country": "US",
|
|
"region": "California",
|
|
"city": city,
|
|
"line1": "1 Infinite Loop",
|
|
"postal_code": "95014",
|
|
"is_default": is_default,
|
|
})
|
|
}
|
|
|
|
async fn create(app: &common::TestApp, token: &str, body: serde_json::Value) -> serde_json::Value {
|
|
let res = client()
|
|
.post(app.url("/api/addresses"))
|
|
.bearer_auth(token)
|
|
.json(&body)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 201, "create failed: {:?}", res.text().await);
|
|
res.json().await.unwrap()
|
|
}
|
|
|
|
async fn list(app: &common::TestApp, token: &str) -> Vec<serde_json::Value> {
|
|
let res = client()
|
|
.get(app.url("/api/addresses"))
|
|
.bearer_auth(token)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
res.json().await.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn address_crud_roundtrip() {
|
|
let app = spawn_app().await;
|
|
let (token, _user_id) = register_customer(&app, "addr-crud").await;
|
|
|
|
let created = create(&app, &token, addr_body("Alice", "Cupertino", false)).await;
|
|
let id = created["id"].as_str().unwrap().to_string();
|
|
assert_eq!(created["city"], "Cupertino");
|
|
// The first address of an account is always the default.
|
|
assert_eq!(created["is_default"], true);
|
|
|
|
let rows = list(&app, &token).await;
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(rows[0]["id"], id);
|
|
|
|
let res = client()
|
|
.put(app.url(&format!("/api/addresses/{id}")))
|
|
.bearer_auth(&token)
|
|
.json(&addr_body("Alice B", "Sunnyvale", false))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let updated: serde_json::Value = res.json().await.unwrap();
|
|
assert_eq!(updated["city"], "Sunnyvale");
|
|
assert_eq!(updated["recipient"], "Alice B");
|
|
|
|
let res = client()
|
|
.delete(app.url(&format!("/api/addresses/{id}")))
|
|
.bearer_auth(&token)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let remaining: Vec<serde_json::Value> = res.json().await.unwrap();
|
|
assert!(remaining.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn single_default_invariant() {
|
|
let app = spawn_app().await;
|
|
let (token, _user_id) = register_customer(&app, "addr-default").await;
|
|
|
|
let first = create(&app, &token, addr_body("A", "Cupertino", true)).await;
|
|
let first_id = first["id"].as_str().unwrap().to_string();
|
|
let second = create(&app, &token, addr_body("B", "Sunnyvale", true)).await;
|
|
let second_id = second["id"].as_str().unwrap().to_string();
|
|
|
|
// Creating a new default must clear the previous one.
|
|
let rows = list(&app, &token).await;
|
|
let defaults: Vec<_> = rows.iter().filter(|r| r["is_default"] == true).collect();
|
|
assert_eq!(defaults.len(), 1);
|
|
assert_eq!(defaults[0]["id"], second_id);
|
|
|
|
// Setting the first one back as default flips the flag atomically.
|
|
let res = client()
|
|
.post(app.url(&format!("/api/addresses/{first_id}/default")))
|
|
.bearer_auth(&token)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let rows = list(&app, &token).await;
|
|
let defaults: Vec<_> = rows.iter().filter(|r| r["is_default"] == true).collect();
|
|
assert_eq!(defaults.len(), 1);
|
|
assert_eq!(defaults[0]["id"], first_id);
|
|
|
|
// Deleting the current default promotes the most recent remaining row.
|
|
let res = client()
|
|
.delete(app.url(&format!("/api/addresses/{first_id}")))
|
|
.bearer_auth(&token)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let remaining: Vec<serde_json::Value> = res.json().await.unwrap();
|
|
assert_eq!(remaining.len(), 1);
|
|
assert_eq!(remaining[0]["id"], second_id);
|
|
assert_eq!(remaining[0]["is_default"], true);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn cross_user_access_is_404() {
|
|
let app = spawn_app().await;
|
|
let (token_a, _) = register_customer(&app, "addr-a").await;
|
|
let (token_b, _) = register_customer(&app, "addr-b").await;
|
|
|
|
let created = create(&app, &token_a, addr_body("A", "Cupertino", true)).await;
|
|
let id = created["id"].as_str().unwrap().to_string();
|
|
|
|
let res = client()
|
|
.put(app.url(&format!("/api/addresses/{id}")))
|
|
.bearer_auth(&token_b)
|
|
.json(&addr_body("Hijack", "Nowhere", true))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 404);
|
|
|
|
let res = client()
|
|
.delete(app.url(&format!("/api/addresses/{id}")))
|
|
.bearer_auth(&token_b)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 404);
|
|
|
|
let res = client()
|
|
.post(app.url(&format!("/api/addresses/{id}/default")))
|
|
.bearer_auth(&token_b)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 404);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn unauthenticated_requests_are_rejected() {
|
|
let app = spawn_app().await;
|
|
for (method, path) in [
|
|
("GET", "/api/addresses".to_string()),
|
|
("POST", "/api/addresses".to_string()),
|
|
(
|
|
"PUT",
|
|
format!("/api/addresses/{}", uuid::Uuid::new_v4()),
|
|
),
|
|
(
|
|
"DELETE",
|
|
format!("/api/addresses/{}", uuid::Uuid::new_v4()),
|
|
),
|
|
] {
|
|
let res = client()
|
|
.request(method.parse().unwrap(), app.url(&path))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 401, "{method} {path} must require auth");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn missing_fields_are_400() {
|
|
let app = spawn_app().await;
|
|
let (token, _) = register_customer(&app, "addr-invalid").await;
|
|
let res = client()
|
|
.post(app.url("/api/addresses"))
|
|
.bearer_auth(&token)
|
|
.json(&serde_json::json!({
|
|
"recipient": "",
|
|
"phone": "+1 555 0100",
|
|
"country": "US",
|
|
"region": "California",
|
|
"city": "Cupertino",
|
|
"line1": "1 Infinite Loop",
|
|
"postal_code": "95014",
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 400);
|
|
}
|