mod common; use common::{client, create_shop, login_admin, make_shop_owner, register_customer, spawn_app}; use serial_test::serial; /// The suite shares one database and creating shops is additive, so each test /// works on shops it creates itself and never asserts on a global count. async fn list_shops(app: &common::TestApp) -> serde_json::Value { let res = client().get(app.url("/api/shops")).send().await.unwrap(); assert_eq!(res.status(), 200, "the directory must be unauthenticated"); res.json().await.unwrap() } fn find<'a>(shops: &'a serde_json::Value, id: &str) -> Option<&'a serde_json::Value> { shops.as_array().unwrap().iter().find(|s| s["id"] == id) } async fn set_profile( app: &common::TestApp, token: &str, shop_id: &str, body: serde_json::Value, ) -> reqwest::Response { client() .put(app.url(&format!("/api/admin/shops/{shop_id}/profile"))) .bearer_auth(token) .json(&body) .send() .await .unwrap() } #[tokio::test] #[serial] async fn shop_without_a_profile_is_still_listed() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-noprofile").await; let shops = list_shops(&app).await; let shop = find(&shops, &shop_id).expect("a shop with no profile must still be listed"); assert!(shop["name"]["en"].is_string()); assert!( shop["logo"].is_null(), "nothing may be invented for a missing profile" ); assert!(shop["score_rating"].is_null()); } #[tokio::test] #[serial] async fn profile_upsert_round_trips_to_the_public_read() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-profile").await; let body = serde_json::json!({ "logo": "/mock/store-1.svg", "company": "Profile Co.", "region": "California", "address": {"en": "1 Market Street", "zh": "市场街 1 号"}, "notice": {"en": "Free shipping over $99.", "zh": "满 99 免运费。"}, "after_sale": {"en": "7-day returns.", "zh": "7 天退货。"}, "score_rating": 4.9, "score_agreement": 4.8, "score_service": 4.7, "score_speed": 4.6 }); let res = set_profile(&app, &admin, &shop_id, body).await; assert_eq!(res.status(), 200, "{:?}", res.text().await); let shop = find(&list_shops(&app).await, &shop_id).unwrap().clone(); assert_eq!(shop["company"], "Profile Co."); assert_eq!(shop["address"]["zh"], "市场街 1 号"); assert_eq!(shop["score_rating"], 4.9); // Public read by slug returns the same composed profile. let slug = shop["slug"].as_str().unwrap(); let res = client() .get(app.url(&format!("/api/shops/{slug}"))) .send() .await .unwrap(); assert_eq!(res.status(), 200); let by_slug: serde_json::Value = res.json().await.unwrap(); assert_eq!(by_slug["id"], shop_id); assert_eq!(by_slug["notice"]["en"], "Free shipping over $99."); } #[tokio::test] #[serial] async fn suspended_or_unknown_shops_are_not_public() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-suspended").await; let shops = list_shops(&app).await; let slug = find(&shops, &shop_id).unwrap()["slug"] .as_str() .unwrap() .to_string(); client() .put(app.url(&format!("/api/admin/shops/{shop_id}/status"))) .bearer_auth(&admin) .json(&serde_json::json!({ "status": "suspended" })) .send() .await .unwrap(); assert!( find(&list_shops(&app).await, &shop_id).is_none(), "suspended shops are hidden" ); let res = client() .get(app.url(&format!("/api/shops/{slug}"))) .send() .await .unwrap(); assert_eq!(res.status(), 404); let res = client() .get(app.url("/api/shops/no-such-shop-at-all")) .send() .await .unwrap(); assert_eq!( res.status(), 404, "an unknown slug is a 404, not an empty profile" ); } #[tokio::test] #[serial] async fn incomplete_bilingual_text_is_refused_without_writing() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-bilingual").await; let good = serde_json::json!({ "company": "Kept Co.", "notice": {"en": "Original notice", "zh": "原始公告"} }); assert_eq!( set_profile(&app, &admin, &shop_id, good).await.status(), 200 ); let bad = serde_json::json!({ "company": "Changed Co.", "notice": {"en": "Only English"} }); let res = set_profile(&app, &admin, &shop_id, bad).await; assert_eq!(res.status(), 400, "a label missing zh must be refused"); let shop = find(&list_shops(&app).await, &shop_id).unwrap().clone(); assert_eq!( shop["company"], "Kept Co.", "the rejected write changed nothing" ); assert_eq!(shop["notice"]["zh"], "原始公告"); } #[tokio::test] #[serial] async fn profile_writes_require_a_platform_admin() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-profile-auth").await; let owner = make_shop_owner(&app, &admin, &shop_id).await; let (customer, _) = register_customer(&app, "shop-profile-cust").await; let body = serde_json::json!({ "company": "Nope" }); for token in [&owner, &customer] { let res = set_profile(&app, token, &shop_id, body.clone()).await; assert_eq!( res.status(), 403, "only platform admins may write a profile" ); } } async fn set_my_profile( app: &common::TestApp, token: &str, body: serde_json::Value, ) -> reqwest::Response { client() .put(app.url("/api/shop/profile")) .bearer_auth(token) .json(&body) .send() .await .unwrap() } #[tokio::test] #[serial] async fn merchant_profile_upsert_round_trips_to_the_public_read() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-self-profile").await; let owner = make_shop_owner(&app, &admin, &shop_id).await; let body = serde_json::json!({ "logo": "/mock/store-self.svg", "company": "Self Co.", "region": "California", "address": {"en": "2 Mission Street", "zh": "米申街 2 号"}, "notice": {"en": "Self-service notice", "zh": "自助公告"}, "after_sale": {"en": "Self returns.", "zh": "自助退货。"} }); let res = set_my_profile(&app, &owner, body).await; assert_eq!(res.status(), 200, "{:?}", res.text().await); let shop = find(&list_shops(&app).await, &shop_id).unwrap().clone(); assert_eq!(shop["company"], "Self Co."); assert_eq!(shop["address"]["zh"], "米申街 2 号"); } #[tokio::test] #[serial] async fn merchant_profile_refuses_incomplete_bilingual_without_writing() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-self-bilingual").await; let owner = make_shop_owner(&app, &admin, &shop_id).await; let good = serde_json::json!({ "company": "Merchant Kept Co.", "notice": {"en": "Merchant notice", "zh": "商家公告"} }); assert_eq!(set_my_profile(&app, &owner, good).await.status(), 200); let bad = serde_json::json!({ "company": "Merchant Changed Co.", "notice": {"en": "Only English"} }); let res = set_my_profile(&app, &owner, bad).await; assert_eq!(res.status(), 400, "a label missing zh must be refused"); let shop = find(&list_shops(&app).await, &shop_id).unwrap().clone(); assert_eq!(shop["company"], "Merchant Kept Co."); } #[tokio::test] #[serial] async fn merchant_profile_never_stores_scores() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_id = create_shop(&app, &admin, "shop-self-scores").await; let owner = make_shop_owner(&app, &admin, &shop_id).await; let admin_body = serde_json::json!({ "company": "Scored Co.", "score_rating": 4.9, "score_service": 4.7 }); assert_eq!(set_profile(&app, &admin, &shop_id, admin_body).await.status(), 200); let merchant_body = serde_json::json!({ "company": "Scored Co. Renamed", "score_rating": 1.0, "score_service": 1.0 }); assert_eq!(set_my_profile(&app, &owner, merchant_body).await.status(), 200); let shop = find(&list_shops(&app).await, &shop_id).unwrap().clone(); assert_eq!(shop["company"], "Scored Co. Renamed"); assert_eq!(shop["score_rating"], 4.9, "merchant writes must not touch scores"); assert_eq!(shop["score_service"], 4.7); } #[tokio::test] #[serial] async fn merchant_profile_requires_a_shop_and_scopes_to_it() { let app = spawn_app().await; let admin = login_admin(&app).await; let shop_a = create_shop(&app, &admin, "shop-self-scope-a").await; let shop_b = create_shop(&app, &admin, "shop-self-scope-b").await; let owner_a = make_shop_owner(&app, &admin, &shop_a).await; let (customer, _) = register_customer(&app, "shop-self-cust").await; let body = serde_json::json!({ "company": "Scoped Co." }); let res = set_my_profile(&app, &customer, body.clone()).await; assert_eq!(res.status(), 403, "a user without a shop must be refused"); assert_eq!(set_my_profile(&app, &owner_a, body).await.status(), 200); let shop_b_view = find(&list_shops(&app).await, &shop_b).unwrap().clone(); assert!( shop_b_view["company"].is_null(), "another shop's profile must stay untouched" ); }