Skip to main content

max / makenotwork

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