Skip to main content

max / makenotwork

5.8 KB · 188 lines History Blame Raw
1 //! Promo code (free access type) generate, list, delete, and claim workflow tests.
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 /// Creator setup: signup, grant, re-login, create project + item, publish both.
7 /// Returns (user_id, item_id).
8 async fn setup_creator_with_item(h: &mut TestHarness) -> (makenotwork::db::UserId, String) {
9 let setup = h.create_creator_with_item("dlseller", "digital", 0).await;
10 h.publish_project_and_item(&setup.project_id, &setup.item_id)
11 .await;
12 (setup.user_id, setup.item_id)
13 }
14
15 #[tokio::test]
16 async fn free_access_code_generate_list_delete() {
17 let mut h = TestHarness::new().await;
18 let (_user_id, item_id) = setup_creator_with_item(&mut h).await;
19
20 // Generate a free access promo code with max_uses (code auto-generated)
21 let resp = h
22 .client
23 .post_form(
24 "/api/promo-codes",
25 &format!("code_purpose=free_access&item_id={item_id}&max_uses=5"),
26 )
27 .await;
28 assert_eq!(
29 resp.status, 200,
30 "Generate free access code failed: {} {}",
31 resp.status, resp.text
32 );
33 let code: Value = resp.json();
34 let code_id = code["id"].as_str().expect("promo code should have id");
35 let key_code = code["code"].as_str().expect("promo code should have code");
36
37 // Verify key code format: 5 or 6 lowercase words separated by dashes.
38 // Generator emits 6 today (raised from 5 after a birthday-collision
39 // review in crypto.rs); validator accepts both for backward compat.
40 let parts: Vec<&str> = key_code.split('-').collect();
41 assert!(
42 matches!(parts.len(), 5 | 6),
43 "KeyCode should have 5 or 6 parts, got {}: {}",
44 parts.len(),
45 key_code
46 );
47 for part in &parts {
48 assert!(!part.is_empty(), "KeyCode parts should be non-empty");
49 assert!(
50 part.chars().all(|c| c.is_ascii_lowercase()),
51 "KeyCode parts should be lowercase: {part}"
52 );
53 }
54
55 // List codes
56 let resp = h.client.get("/api/promo-codes").await;
57 assert_eq!(
58 resp.status, 200,
59 "List promo codes failed: {} {}",
60 resp.status, resp.text
61 );
62 let list: Value = resp.json();
63 let data = list["data"].as_array().expect("data should be array");
64 assert!(!data.is_empty());
65
66 // Delete code
67 let resp = h
68 .client
69 .delete(&format!("/api/promo-codes/{code_id}"))
70 .await;
71 assert_eq!(resp.status, 204, "Delete should return 204");
72 }
73
74 #[tokio::test]
75 async fn free_access_code_claim_by_buyer() {
76 let mut h = TestHarness::new().await;
77 let (_user_id, item_id) = setup_creator_with_item(&mut h).await;
78
79 // Generate a free access code
80 let resp = h
81 .client
82 .post_form(
83 "/api/promo-codes",
84 &format!("code_purpose=free_access&item_id={item_id}"),
85 )
86 .await;
87 assert_eq!(resp.status, 200, "Generate code failed: {}", resp.text);
88 let code: Value = resp.json();
89 let key_code = code["code"].as_str().unwrap();
90
91 // Switch to buyer
92 h.client.post_form("/logout", "").await;
93 let _buyer_id = h.signup("dlbuyer", "dlbuyer@test.com", "password456").await;
94
95 // Claim the promo code
96 let resp = h
97 .client
98 .post_form("/api/promo-codes/claim", &format!("code={key_code}"))
99 .await;
100 assert_eq!(
101 resp.status, 200,
102 "Claim failed: {} {}",
103 resp.status, resp.text
104 );
105 let claim: Value = resp.json();
106 assert!(claim["success"].as_bool().unwrap());
107 assert!(!claim["already_owned"].as_bool().unwrap());
108 assert_eq!(claim["item_id"].as_str().unwrap(), item_id);
109
110 // Verify item appears in library
111 let resp = h.client.get("/library").await;
112 assert_eq!(resp.status, 200);
113 assert!(
114 resp.text.contains("Test Item"),
115 "Library should contain the claimed item"
116 );
117 }
118
119 #[tokio::test]
120 async fn free_access_code_claim_already_owned() {
121 let mut h = TestHarness::new().await;
122 let (_user_id, item_id) = setup_creator_with_item(&mut h).await;
123
124 // Generate a free access code (unlimited uses)
125 let resp = h
126 .client
127 .post_form(
128 "/api/promo-codes",
129 &format!("code_purpose=free_access&item_id={item_id}"),
130 )
131 .await;
132 let code: Value = resp.json();
133 let key_code = code["code"].as_str().unwrap();
134
135 // Switch to buyer
136 h.client.post_form("/logout", "").await;
137 let _buyer_id = h
138 .signup("dlbuyer2", "dlbuyer2@test.com", "password456")
139 .await;
140
141 // First claim
142 let resp = h
143 .client
144 .post_form("/api/promo-codes/claim", &format!("code={key_code}"))
145 .await;
146 assert_eq!(resp.status, 200, "{}", resp.text);
147 let claim: Value = resp.json();
148 assert!(!claim["already_owned"].as_bool().unwrap());
149
150 // Second claim of same code
151 let resp = h
152 .client
153 .post_form("/api/promo-codes/claim", &format!("code={key_code}"))
154 .await;
155 assert_eq!(resp.status, 200, "{}", resp.text);
156 let claim: Value = resp.json();
157 assert!(
158 claim["already_owned"].as_bool().unwrap(),
159 "Second claim should report already_owned"
160 );
161 }
162
163 #[tokio::test]
164 async fn free_access_code_claim_invalid_code() {
165 let mut h = TestHarness::new().await;
166 let _buyer_id = h
167 .signup("dlbuyer3", "dlbuyer3@test.com", "password456")
168 .await;
169
170 // Try to claim a nonexistent but valid-format code
171 let resp = h
172 .client
173 .post_form(
174 "/api/promo-codes/claim",
175 "code=bright-castle-forest-river-falcon",
176 )
177 .await;
178 assert_eq!(
179 resp.status, 400,
180 "Invalid code should return 400, got {}: {}",
181 resp.status, resp.text
182 );
183 assert!(
184 resp.text.contains("Invalid") || resp.text.contains("invalid"),
185 "Error should mention invalid code"
186 );
187 }
188