Skip to main content

max / makenotwork

11.3 KB · 396 lines History Blame Raw
1 //! License key workflow: create item -> enable keys -> generate key ->
2 //! validate -> deactivate -> check status
3
4 use crate::harness::TestHarness;
5 use serde_json::Value;
6
7 #[tokio::test]
8 async fn license_key_lifecycle() {
9 let mut h = TestHarness::new().await;
10
11 // Setup: creator with project and item
12 let user_id = h
13 .signup("keymaker", "keymaker@example.com", "password123")
14 .await;
15 h.grant_creator(user_id).await;
16 h.client.post_form("/logout", "").await;
17 h.login("keymaker", "password123").await;
18
19 let resp = h
20 .client
21 .post_form("/api/projects", "slug=software&title=Software")
22 .await;
23 let project: Value = resp.json();
24 let project_id = project["id"].as_str().unwrap();
25
26 let resp = h
27 .client
28 .post_form(
29 &format!("/api/projects/{}/items", project_id),
30 "title=My+Plugin&price_cents=0&item_type=plugin",
31 )
32 .await;
33 let item: Value = resp.json();
34 let item_id = item["id"].as_str().unwrap();
35
36 // Enable license keys with max 3 activations
37 let resp = h
38 .client
39 .put_form(
40 &format!("/api/items/{}/license-settings", item_id),
41 "enable_license_keys=on&default_max_activations=3",
42 )
43 .await;
44 assert!(
45 resp.status.is_success(),
46 "Enable license keys failed: {} {}",
47 resp.status,
48 resp.text
49 );
50
51 // Generate a license key
52 let resp = h
53 .client
54 .post_form(&format!("/api/items/{}/keys", item_id), "")
55 .await;
56 assert!(
57 resp.status.is_success(),
58 "Generate key failed: {} {}",
59 resp.status,
60 resp.text
61 );
62 let key: Value = resp.json();
63 let key_code = key["key_code"].as_str().expect("key should have key_code");
64
65 // Validate the key (public endpoint — no auth required)
66 let resp = h
67 .client
68 .post_json(
69 "/api/keys/validate",
70 &format!(
71 r#"{{"key": "{}", "machine_id": "machine-001", "label": "My Laptop"}}"#,
72 key_code
73 ),
74 )
75 .await;
76 assert!(resp.status.is_success(), "Validate key failed: {}", resp.text);
77 let validation: Value = resp.json();
78 assert_eq!(validation["valid"], true, "Key should be valid");
79
80 // Check key status
81 let resp = h
82 .client
83 .get(&format!("/api/keys/{}/status", key_code))
84 .await;
85 assert!(resp.status.is_success(), "Key status failed: {}", resp.text);
86 let status: Value = resp.json();
87 assert_eq!(status["valid"], true);
88 assert_eq!(
89 status["license"]["activation_count"], 1,
90 "Should have 1 activation"
91 );
92
93 // Deactivate
94 let resp = h
95 .client
96 .post_json(
97 "/api/keys/deactivate",
98 &format!(
99 r#"{{"key": "{}", "machine_id": "machine-001"}}"#,
100 key_code
101 ),
102 )
103 .await;
104 assert!(
105 resp.status.is_success(),
106 "Deactivate key failed: {}",
107 resp.text
108 );
109 let deactivation: Value = resp.json();
110 assert_eq!(deactivation["success"], true);
111
112 // Check status again — activation count should be 0
113 let resp = h
114 .client
115 .get(&format!("/api/keys/{}/status", key_code))
116 .await;
117 let status: Value = resp.json();
118 assert_eq!(
119 status["license"]["activation_count"], 0,
120 "Should have 0 activations after deactivation"
121 );
122 }
123
124 /// Helper: create a creator with a project and license-key-enabled item.
125 /// Returns (item_id, key_code) after generating one key.
126 async fn setup_creator_with_item(
127 h: &mut TestHarness,
128 username: &str,
129 max_activations: u32,
130 ) -> String {
131 let user_id = h
132 .signup(username, &format!("{}@example.com", username), "password123")
133 .await;
134 h.grant_creator(user_id).await;
135 h.client.post_form("/logout", "").await;
136 h.login(username, "password123").await;
137
138 let resp = h
139 .client
140 .post_form("/api/projects", "slug=software&title=Software")
141 .await;
142 let project: Value = resp.json();
143 let project_id = project["id"].as_str().unwrap();
144
145 let resp = h
146 .client
147 .post_form(
148 &format!("/api/projects/{}/items", project_id),
149 "title=My+Plugin&price_cents=0&item_type=plugin",
150 )
151 .await;
152 let item: Value = resp.json();
153 let item_id = item["id"].as_str().unwrap().to_string();
154
155 let resp = h
156 .client
157 .put_form(
158 &format!("/api/items/{}/license-settings", item_id),
159 &format!(
160 "enable_license_keys=on&default_max_activations={}",
161 max_activations
162 ),
163 )
164 .await;
165 assert!(resp.status.is_success(), "Enable license keys failed: {}", resp.text);
166
167 item_id
168 }
169
170 async fn generate_key(h: &mut TestHarness, item_id: &str) -> Value {
171 let resp = h
172 .client
173 .post_form(&format!("/api/items/{}/keys", item_id), "")
174 .await;
175 assert!(resp.status.is_success(), "Generate key failed: {}", resp.text);
176 resp.json()
177 }
178
179 #[tokio::test]
180 async fn max_activations_enforced() {
181 let mut h = TestHarness::new().await;
182 let item_id = setup_creator_with_item(&mut h, "maxact", 2).await;
183 let key = generate_key(&mut h, &item_id).await;
184 let key_code = key["key_code"].as_str().unwrap();
185
186 // First activation — should succeed
187 let resp = h
188 .client
189 .post_json(
190 "/api/keys/validate",
191 &format!(
192 r#"{{"key": "{}", "machine_id": "machine-001", "label": "Machine 1"}}"#,
193 key_code
194 ),
195 )
196 .await;
197 assert!(resp.status.is_success());
198 let v: Value = resp.json();
199 assert_eq!(v["valid"], true, "First activation should succeed");
200
201 // Second activation — should succeed
202 let resp = h
203 .client
204 .post_json(
205 "/api/keys/validate",
206 &format!(
207 r#"{{"key": "{}", "machine_id": "machine-002", "label": "Machine 2"}}"#,
208 key_code
209 ),
210 )
211 .await;
212 assert!(resp.status.is_success());
213 let v: Value = resp.json();
214 assert_eq!(v["valid"], true, "Second activation should succeed");
215
216 // Third activation — should fail (limit is 2)
217 let resp = h
218 .client
219 .post_json(
220 "/api/keys/validate",
221 &format!(
222 r#"{{"key": "{}", "machine_id": "machine-003", "label": "Machine 3"}}"#,
223 key_code
224 ),
225 )
226 .await;
227 let v: Value = resp.json();
228 assert_eq!(
229 v["valid"], false,
230 "Third activation should be rejected (max=2)"
231 );
232 }
233
234 #[tokio::test]
235 async fn invalid_key_rejected() {
236 let mut h = TestHarness::new().await;
237
238 let resp = h
239 .client
240 .post_json(
241 "/api/keys/validate",
242 r#"{"key": "BOGUS-KEY-DOES-NOT-EXIST", "machine_id": "m1", "label": "Test"}"#,
243 )
244 .await;
245 // KeyCode deserialization rejects invalid format before reaching the handler
246 assert!(
247 resp.status.is_client_error(),
248 "Bogus key should be rejected: {} {}",
249 resp.status, resp.text
250 );
251 }
252
253 #[tokio::test]
254 async fn revoke_key_then_validate_fails() {
255 let mut h = TestHarness::new().await;
256 let item_id = setup_creator_with_item(&mut h, "revoker", 3).await;
257 let key = generate_key(&mut h, &item_id).await;
258 let key_code = key["key_code"].as_str().unwrap();
259
260 // Validate first — should succeed
261 let resp = h
262 .client
263 .post_json(
264 "/api/keys/validate",
265 &format!(
266 r#"{{"key": "{}", "machine_id": "machine-001", "label": "Laptop"}}"#,
267 key_code
268 ),
269 )
270 .await;
271 assert!(resp.status.is_success());
272 let v: Value = resp.json();
273 assert_eq!(v["valid"], true);
274
275 // Get the key's database ID — try the response first, fall back to DB query
276 let key_id = if let Some(id) = key["id"].as_str() {
277 id.to_string()
278 } else {
279 let row: (sqlx::types::Uuid,) =
280 sqlx::query_as("SELECT id FROM license_keys WHERE key_code = $1")
281 .bind(key_code)
282 .fetch_one(&h.db)
283 .await
284 .expect("Key should exist in DB");
285 row.0.to_string()
286 };
287
288 // Revoke the key (creator-only endpoint)
289 let resp = h
290 .client
291 .post_form(&format!("/api/keys/{}/revoke", key_id), "")
292 .await;
293 assert!(
294 resp.status.is_success(),
295 "Revoke key failed: {} {}",
296 resp.status,
297 resp.text
298 );
299
300 // Validate again — should fail
301 let resp = h
302 .client
303 .post_json(
304 "/api/keys/validate",
305 &format!(
306 r#"{{"key": "{}", "machine_id": "machine-002", "label": "Desktop"}}"#,
307 key_code
308 ),
309 )
310 .await;
311 let v: Value = resp.json();
312 assert_eq!(v["valid"], false, "Revoked key should not validate");
313 }
314
315 #[tokio::test]
316 async fn list_keys() {
317 let mut h = TestHarness::new().await;
318 let item_id = setup_creator_with_item(&mut h, "lister", 3).await;
319
320 // Generate 3 keys
321 for _ in 0..3 {
322 generate_key(&mut h, &item_id).await;
323 }
324
325 let resp = h
326 .client
327 .get(&format!("/api/items/{}/keys", item_id))
328 .await;
329 assert!(resp.status.is_success(), "List keys failed: {}", resp.text);
330 let body: Value = resp.json();
331 let arr = body["data"].as_array().expect("Response should have a 'data' array");
332 assert_eq!(arr.len(), 3, "Should have 3 keys");
333 }
334
335 #[tokio::test]
336 async fn v1_license_endpoints() {
337 let mut h = TestHarness::new().await;
338 let item_id = setup_creator_with_item(&mut h, "v1user", 3).await;
339 let key = generate_key(&mut h, &item_id).await;
340 let key_code = key["key_code"].as_str().unwrap();
341
342 // Enable license verification on the project (required for v1 verify)
343 let project_id: uuid::Uuid = sqlx::query_scalar(
344 "SELECT project_id FROM items WHERE id = $1::uuid",
345 )
346 .bind(&item_id)
347 .fetch_one(&h.db)
348 .await
349 .unwrap();
350 sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1")
351 .bind(project_id)
352 .execute(&h.db)
353 .await
354 .unwrap();
355
356 // POST /api/v1/license/verify
357 let resp = h
358 .client
359 .post_json(
360 "/api/v1/license/verify",
361 &format!(
362 r#"{{"key": "{}", "machine_fingerprint": "machine-v1"}}"#,
363 key_code
364 ),
365 )
366 .await;
367 assert!(
368 resp.status.is_success(),
369 "v1 verify failed: {} {}",
370 resp.status,
371 resp.text
372 );
373 let v: Value = resp.json();
374 assert_eq!(v["valid"], true, "v1 verify should return valid=true");
375
376 // POST /api/v1/license/deactivate
377 let resp = h
378 .client
379 .post_json(
380 "/api/v1/license/deactivate",
381 &format!(
382 r#"{{"key": "{}", "machine_fingerprint": "machine-v1"}}"#,
383 key_code
384 ),
385 )
386 .await;
387 assert!(
388 resp.status.is_success(),
389 "v1 deactivate failed: {} {}",
390 resp.status,
391 resp.text
392 );
393 let v: Value = resp.json();
394 assert_eq!(v["success"], true, "v1 deactivate should return success=true");
395 }
396