//! Promo code (free access type) generate, list, delete, and claim workflow tests. use crate::harness::TestHarness; use serde_json::Value; /// Creator setup: signup, grant, re-login, create project + item, publish both. /// Returns (user_id, item_id). async fn setup_creator_with_item(h: &mut TestHarness) -> (makenotwork::db::UserId, String) { let setup = h.create_creator_with_item("dlseller", "digital", 0).await; h.publish_project_and_item(&setup.project_id, &setup.item_id).await; (setup.user_id, setup.item_id) } #[tokio::test] async fn free_access_code_generate_list_delete() { let mut h = TestHarness::new().await; let (_user_id, item_id) = setup_creator_with_item(&mut h).await; // Generate a free access promo code with max_uses (code auto-generated) let resp = h.client.post_form( "/api/promo-codes", &format!("code_purpose=free_access&item_id={}&max_uses=5", item_id), ).await; assert!(resp.status.is_success(), "Generate free access code failed: {} {}", resp.status, resp.text); let code: Value = resp.json(); let code_id = code["id"].as_str().expect("promo code should have id"); let key_code = code["code"].as_str().expect("promo code should have code"); // Verify key code format: 5 or 6 lowercase words separated by dashes. // Generator emits 6 today (raised from 5 after a birthday-collision // review in crypto.rs); validator accepts both for backward compat. let parts: Vec<&str> = key_code.split('-').collect(); assert!( matches!(parts.len(), 5 | 6), "KeyCode should have 5 or 6 parts, got {}: {}", parts.len(), key_code ); for part in &parts { assert!(!part.is_empty(), "KeyCode parts should be non-empty"); assert!(part.chars().all(|c| c.is_ascii_lowercase()), "KeyCode parts should be lowercase: {}", part); } // List codes let resp = h.client.get("/api/promo-codes").await; assert!(resp.status.is_success(), "List promo codes failed: {} {}", resp.status, resp.text); let list: Value = resp.json(); let data = list["data"].as_array().expect("data should be array"); assert!(!data.is_empty()); // Delete code let resp = h.client.delete(&format!("/api/promo-codes/{}", code_id)).await; assert_eq!(resp.status, 204, "Delete should return 204"); } #[tokio::test] async fn free_access_code_claim_by_buyer() { let mut h = TestHarness::new().await; let (_user_id, item_id) = setup_creator_with_item(&mut h).await; // Generate a free access code let resp = h.client.post_form( "/api/promo-codes", &format!("code_purpose=free_access&item_id={}", item_id), ).await; assert!(resp.status.is_success(), "Generate code failed: {}", resp.text); let code: Value = resp.json(); let key_code = code["code"].as_str().unwrap(); // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("dlbuyer", "dlbuyer@test.com", "password456").await; // Claim the promo code let resp = h.client.post_form( "/api/promo-codes/claim", &format!("code={}", key_code), ).await; assert!(resp.status.is_success(), "Claim failed: {} {}", resp.status, resp.text); let claim: Value = resp.json(); assert!(claim["success"].as_bool().unwrap()); assert!(!claim["already_owned"].as_bool().unwrap()); assert_eq!(claim["item_id"].as_str().unwrap(), item_id); // Verify item appears in library let resp = h.client.get("/library").await; assert_eq!(resp.status, 200); assert!(resp.text.contains("Test Item"), "Library should contain the claimed item"); } #[tokio::test] async fn free_access_code_claim_already_owned() { let mut h = TestHarness::new().await; let (_user_id, item_id) = setup_creator_with_item(&mut h).await; // Generate a free access code (unlimited uses) let resp = h.client.post_form( "/api/promo-codes", &format!("code_purpose=free_access&item_id={}", item_id), ).await; let code: Value = resp.json(); let key_code = code["code"].as_str().unwrap(); // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("dlbuyer2", "dlbuyer2@test.com", "password456").await; // First claim let resp = h.client.post_form( "/api/promo-codes/claim", &format!("code={}", key_code), ).await; assert!(resp.status.is_success()); let claim: Value = resp.json(); assert!(!claim["already_owned"].as_bool().unwrap()); // Second claim of same code let resp = h.client.post_form( "/api/promo-codes/claim", &format!("code={}", key_code), ).await; assert!(resp.status.is_success()); let claim: Value = resp.json(); assert!(claim["already_owned"].as_bool().unwrap(), "Second claim should report already_owned"); } #[tokio::test] async fn free_access_code_claim_invalid_code() { let mut h = TestHarness::new().await; let _buyer_id = h.signup("dlbuyer3", "dlbuyer3@test.com", "password456").await; // Try to claim a nonexistent but valid-format code let resp = h.client.post_form( "/api/promo-codes/claim", "code=bright-castle-forest-river-falcon", ).await; assert_eq!(resp.status, 400, "Invalid code should return 400, got {}: {}", resp.status, resp.text); assert!(resp.text.contains("Invalid") || resp.text.contains("invalid"), "Error should mention invalid code"); }