205 lines
6.2 KiB
Rust
205 lines
6.2 KiB
Rust
mod common;
|
|
|
|
use common::{
|
|
client, create_product_with_sku, create_shop, login_admin, make_shop_owner, register_customer,
|
|
spawn_app,
|
|
};
|
|
use serial_test::serial;
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn customer_forbidden_on_admin_routes() {
|
|
let app = spawn_app().await;
|
|
let (customer_token, _) = register_customer(&app, "cust").await;
|
|
let res = client()
|
|
.get(app.url("/api/admin/users"))
|
|
.bearer_auth(&customer_token)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 403);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn publish_lifecycle_and_public_visibility() {
|
|
let app = spawn_app().await;
|
|
let admin = login_admin(&app).await;
|
|
let shop_id = create_shop(&app, &admin, "shop-life").await;
|
|
let owner = make_shop_owner(&app, &admin, &shop_id).await;
|
|
|
|
// draft with no SKU cannot be published
|
|
let res = client()
|
|
.post(app.url("/api/shop/products"))
|
|
.bearer_auth(&owner)
|
|
.json(&serde_json::json!({
|
|
"slug": "nosku",
|
|
"name": {"en": "NoSku", "zh": "无SKU"},
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let no_sku_id = res.json::<serde_json::Value>().await.unwrap()["id"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_string();
|
|
let res = client()
|
|
.post(app.url(&format!("/api/shop/products/{no_sku_id}/publish")))
|
|
.bearer_auth(&owner)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 400);
|
|
|
|
// product with SKU: publish → listed publicly → unpublish → gone
|
|
let (product_id, mug_slug) = create_product_with_sku(&app, &owner, "mug", 1299, 10).await;
|
|
let res = client()
|
|
.post(app.url(&format!("/api/shop/products/{product_id}/publish")))
|
|
.bearer_auth(&owner)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
assert_eq!(res.json::<serde_json::Value>().await.unwrap()["status"], "published");
|
|
|
|
let res = client().get(app.url("/api/products")).send().await.unwrap();
|
|
let list: serde_json::Value = res.json().await.unwrap();
|
|
assert!(list["items"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p["id"] == product_id));
|
|
|
|
// detail by slug works and carries i18n fields
|
|
let res = client()
|
|
.get(app.url(&format!("/api/products/{mug_slug}")))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let detail: serde_json::Value = res.json().await.unwrap();
|
|
assert_eq!(detail["name"]["en"], format!("Product {mug_slug}"));
|
|
assert_eq!(detail["name"]["zh"], format!("商品 {mug_slug}"));
|
|
assert_eq!(detail["skus"][0]["price_minor"], 1299);
|
|
|
|
let res = client()
|
|
.post(app.url(&format!("/api/shop/products/{product_id}/unpublish")))
|
|
.bearer_auth(&owner)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let res = client()
|
|
.get(app.url(&format!("/api/products/{product_id}")))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 404);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn shop_isolation_enforced() {
|
|
let app = spawn_app().await;
|
|
let admin = login_admin(&app).await;
|
|
let shop_a = create_shop(&app, &admin, "shop-iso-a").await;
|
|
let shop_b = create_shop(&app, &admin, "shop-iso-b").await;
|
|
let owner_a = make_shop_owner(&app, &admin, &shop_a).await;
|
|
let owner_b = make_shop_owner(&app, &admin, &shop_b).await;
|
|
|
|
let (product_b, _) = create_product_with_sku(&app, &owner_b, "b-item", 500, 3).await;
|
|
let res = client()
|
|
.get(app.url(&format!("/api/shop/products/{product_b}")))
|
|
.bearer_auth(&owner_a)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 404);
|
|
|
|
// owner A's product list contains none of shop B's products
|
|
let res = client()
|
|
.get(app.url("/api/shop/products"))
|
|
.bearer_auth(&owner_a)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let list: serde_json::Value = res.json().await.unwrap();
|
|
assert!(!list["items"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p["id"] == product_b));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn currency_conversion_math() {
|
|
let app = spawn_app().await;
|
|
// 1000 minor USD ($10.00) -> JPY at rate 150 => 1500 minor (¥1500)
|
|
let res = client()
|
|
.get(app.url("/api/currencies/convert?amount_minor=1000&from=USD&to=JPY"))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let body: serde_json::Value = res.json().await.unwrap();
|
|
assert_eq!(body["amount_minor"], 1500);
|
|
assert_eq!(body["currency"], "JPY");
|
|
|
|
// disabled currency rejected
|
|
let admin = login_admin(&app).await;
|
|
let res = client()
|
|
.post(app.url("/api/admin/currencies"))
|
|
.bearer_auth(&admin)
|
|
.json(&serde_json::json!({
|
|
"code": "XXX",
|
|
"name": {"en": "Test", "zh": "测试"},
|
|
"symbol": "X",
|
|
"exponent": 2,
|
|
"rate_to_base": "2.0",
|
|
"enabled": false,
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 200);
|
|
let res = client()
|
|
.get(app.url("/api/currencies/convert?amount_minor=100&from=USD&to=XXX"))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), 400);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn suspended_shop_hidden_from_public_catalog() {
|
|
let app = spawn_app().await;
|
|
let admin = login_admin(&app).await;
|
|
let shop_id = create_shop(&app, &admin, "shop-susp").await;
|
|
let owner = make_shop_owner(&app, &admin, &shop_id).await;
|
|
let (product_id, _) = create_product_with_sku(&app, &owner, "susp-item", 100, 1).await;
|
|
client()
|
|
.post(app.url(&format!("/api/shop/products/{product_id}/publish")))
|
|
.bearer_auth(&owner)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
client()
|
|
.put(app.url(&format!("/api/admin/shops/{shop_id}/status")))
|
|
.bearer_auth(&admin)
|
|
.json(&serde_json::json!({ "status": "suspended" }))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
let res = client().get(app.url("/api/products")).send().await.unwrap();
|
|
let list: serde_json::Value = res.json().await.unwrap();
|
|
assert!(!list["items"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p["id"] == product_id));
|
|
}
|