Files
vmall/apps/api/tests/shops.rs
T

298 lines
9.7 KiB
Rust

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"
);
}