Skip to main content

max / makenotwork

14.6 KB · 494 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/{project_id}/items"),
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/{item_id}/license-settings"),
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/{item_id}/keys"), "")
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": "{key_code}", "machine_id": "machine-001", "label": "My Laptop"}}"#
72 ),
73 )
74 .await;
75 assert!(
76 resp.status.is_success(),
77 "Validate key failed: {}",
78 resp.text
79 );
80 let validation: Value = resp.json();
81 assert_eq!(validation["valid"], true, "Key should be valid");
82
83 // Check key status
84 let resp = h.client.get(&format!("/api/keys/{key_code}/status")).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 // The POST form (key in the body, not the URL) must return the same thing,
94 // it exists so the purchase-proof key doesn't land in access logs.
95 let resp = h
96 .client
97 .post_json("/api/keys/status", &format!(r#"{{"key": "{key_code}"}}"#))
98 .await;
99 assert!(
100 resp.status.is_success(),
101 "POST key status failed: {}",
102 resp.text
103 );
104 let post_status: Value = resp.json();
105 assert_eq!(post_status["valid"], true);
106 assert_eq!(post_status["license"]["activation_count"], 1);
107
108 // Deactivate
109 let resp = h
110 .client
111 .post_json(
112 "/api/keys/deactivate",
113 &format!(r#"{{"key": "{key_code}", "machine_id": "machine-001"}}"#),
114 )
115 .await;
116 assert!(
117 resp.status.is_success(),
118 "Deactivate key failed: {}",
119 resp.text
120 );
121 let deactivation: Value = resp.json();
122 assert_eq!(deactivation["success"], true);
123
124 // Check status again, activation count should be 0
125 let resp = h.client.get(&format!("/api/keys/{key_code}/status")).await;
126 let status: Value = resp.json();
127 assert_eq!(
128 status["license"]["activation_count"], 0,
129 "Should have 0 activations after deactivation"
130 );
131 }
132
133 /// Helper: create a creator with a project and license-key-enabled item.
134 /// Returns (item_id, key_code) after generating one key.
135 async fn setup_creator_with_item(
136 h: &mut TestHarness,
137 username: &str,
138 max_activations: u32,
139 ) -> String {
140 let user_id = h
141 .signup(username, &format!("{username}@example.com"), "password123")
142 .await;
143 h.grant_creator(user_id).await;
144 h.client.post_form("/logout", "").await;
145 h.login(username, "password123").await;
146
147 let resp = h
148 .client
149 .post_form("/api/projects", "slug=software&title=Software")
150 .await;
151 let project: Value = resp.json();
152 let project_id = project["id"].as_str().unwrap();
153
154 let resp = h
155 .client
156 .post_form(
157 &format!("/api/projects/{project_id}/items"),
158 "title=My+Plugin&price_cents=0&item_type=plugin",
159 )
160 .await;
161 let item: Value = resp.json();
162 let item_id = item["id"].as_str().unwrap().to_string();
163
164 let resp = h
165 .client
166 .put_form(
167 &format!("/api/items/{item_id}/license-settings"),
168 &format!("enable_license_keys=on&default_max_activations={max_activations}"),
169 )
170 .await;
171 assert!(
172 resp.status.is_success(),
173 "Enable license keys failed: {}",
174 resp.text
175 );
176
177 item_id
178 }
179
180 async fn generate_key(h: &mut TestHarness, item_id: &str) -> Value {
181 let resp = h
182 .client
183 .post_form(&format!("/api/items/{item_id}/keys"), "")
184 .await;
185 assert!(
186 resp.status.is_success(),
187 "Generate key failed: {}",
188 resp.text
189 );
190 resp.json()
191 }
192
193 #[tokio::test]
194 async fn max_activations_enforced() {
195 let mut h = TestHarness::new().await;
196 let item_id = setup_creator_with_item(&mut h, "maxact", 2).await;
197 let key = generate_key(&mut h, &item_id).await;
198 let key_code = key["key_code"].as_str().unwrap();
199
200 // First activation, should succeed
201 let resp = h
202 .client
203 .post_json(
204 "/api/keys/validate",
205 &format!(
206 r#"{{"key": "{key_code}", "machine_id": "machine-001", "label": "Machine 1"}}"#
207 ),
208 )
209 .await;
210 assert!(resp.status.is_success());
211 let v: Value = resp.json();
212 assert_eq!(v["valid"], true, "First activation should succeed");
213
214 // Second activation, should succeed
215 let resp = h
216 .client
217 .post_json(
218 "/api/keys/validate",
219 &format!(
220 r#"{{"key": "{key_code}", "machine_id": "machine-002", "label": "Machine 2"}}"#
221 ),
222 )
223 .await;
224 assert!(resp.status.is_success());
225 let v: Value = resp.json();
226 assert_eq!(v["valid"], true, "Second activation should succeed");
227
228 // Third activation, should fail (limit is 2)
229 let resp = h
230 .client
231 .post_json(
232 "/api/keys/validate",
233 &format!(
234 r#"{{"key": "{key_code}", "machine_id": "machine-003", "label": "Machine 3"}}"#
235 ),
236 )
237 .await;
238 let v: Value = resp.json();
239 assert_eq!(
240 v["valid"], false,
241 "Third activation should be rejected (max=2)"
242 );
243 }
244
245 #[tokio::test]
246 async fn invalid_key_rejected() {
247 let mut h = TestHarness::new().await;
248
249 let resp = h
250 .client
251 .post_json(
252 "/api/keys/validate",
253 r#"{"key": "BOGUS-KEY-DOES-NOT-EXIST", "machine_id": "m1", "label": "Test"}"#,
254 )
255 .await;
256 // KeyCode deserialization rejects invalid format before reaching the handler
257 assert!(
258 resp.status.is_client_error(),
259 "Bogus key should be rejected: {} {}",
260 resp.status,
261 resp.text
262 );
263 }
264
265 #[tokio::test]
266 async fn revoke_key_then_validate_fails() {
267 let mut h = TestHarness::new().await;
268 let item_id = setup_creator_with_item(&mut h, "revoker", 3).await;
269 let key = generate_key(&mut h, &item_id).await;
270 let key_code = key["key_code"].as_str().unwrap();
271
272 // Validate first, should succeed
273 let resp = h
274 .client
275 .post_json(
276 "/api/keys/validate",
277 &format!(r#"{{"key": "{key_code}", "machine_id": "machine-001", "label": "Laptop"}}"#),
278 )
279 .await;
280 assert!(resp.status.is_success());
281 let v: Value = resp.json();
282 assert_eq!(v["valid"], true);
283
284 // Get the key's database ID, try the response first, fall back to DB query
285 let key_id = if let Some(id) = key["id"].as_str() {
286 id.to_string()
287 } else {
288 let row: (sqlx::types::Uuid,) =
289 sqlx::query_as("SELECT id FROM license_keys WHERE key_code = $1")
290 .bind(key_code)
291 .fetch_one(&h.db)
292 .await
293 .expect("Key should exist in DB");
294 row.0.to_string()
295 };
296
297 // Revoke the key (creator-only endpoint)
298 let resp = h
299 .client
300 .post_form(&format!("/api/keys/{key_id}/revoke"), "")
301 .await;
302 assert!(
303 resp.status.is_success(),
304 "Revoke key failed: {} {}",
305 resp.status,
306 resp.text
307 );
308
309 // Validate again, should fail
310 let resp = h
311 .client
312 .post_json(
313 "/api/keys/validate",
314 &format!(r#"{{"key": "{key_code}", "machine_id": "machine-002", "label": "Desktop"}}"#),
315 )
316 .await;
317 let v: Value = resp.json();
318 assert_eq!(v["valid"], false, "Revoked key should not validate");
319 }
320
321 #[tokio::test]
322 async fn list_keys() {
323 let mut h = TestHarness::new().await;
324 let item_id = setup_creator_with_item(&mut h, "lister", 3).await;
325
326 // Generate 3 keys
327 for _ in 0..3 {
328 generate_key(&mut h, &item_id).await;
329 }
330
331 let resp = h.client.get(&format!("/api/items/{item_id}/keys")).await;
332 assert!(resp.status.is_success(), "List keys failed: {}", resp.text);
333 let body: Value = resp.json();
334 let arr = body["data"]
335 .as_array()
336 .expect("Response should have a 'data' array");
337 assert_eq!(arr.len(), 3, "Should have 3 keys");
338 }
339
340 #[tokio::test]
341 async fn v1_license_endpoints() {
342 let mut h = TestHarness::new().await;
343 let item_id = setup_creator_with_item(&mut h, "v1user", 3).await;
344 let key = generate_key(&mut h, &item_id).await;
345 let key_code = key["key_code"].as_str().unwrap();
346
347 // Enable license verification on the project (required for v1 verify)
348 let project_id: uuid::Uuid =
349 sqlx::query_scalar("SELECT project_id FROM items WHERE id = $1::uuid")
350 .bind(&item_id)
351 .fetch_one(&h.db)
352 .await
353 .unwrap();
354 sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1")
355 .bind(project_id)
356 .execute(&h.db)
357 .await
358 .unwrap();
359
360 // POST /api/v1/license/verify
361 let resp = h
362 .client
363 .post_json(
364 "/api/v1/license/verify",
365 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-v1"}}"#),
366 )
367 .await;
368 assert!(
369 resp.status.is_success(),
370 "v1 verify failed: {} {}",
371 resp.status,
372 resp.text
373 );
374 let v: Value = resp.json();
375 assert_eq!(v["valid"], true, "v1 verify should return valid=true");
376
377 // POST /api/v1/license/deactivate
378 let resp = h
379 .client
380 .post_json(
381 "/api/v1/license/deactivate",
382 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-v1"}}"#),
383 )
384 .await;
385 assert!(
386 resp.status.is_success(),
387 "v1 deactivate failed: {} {}",
388 resp.status,
389 resp.text
390 );
391 let v: Value = resp.json();
392 assert_eq!(
393 v["success"], true,
394 "v1 deactivate should return success=true"
395 );
396 }
397
398 /// Concurrent activation race: for a single-seat key, many machines activating
399 /// at once must resolve to exactly one winner. `try_create_activation` takes the
400 /// `license_keys` row `FOR UPDATE` and recomputes the count under the lock, so
401 /// the cap holds even when every request reads the pre-lock count of 0.
402 #[tokio::test]
403 async fn concurrent_activation_race_respects_single_seat() {
404 use makenotwork::db::{self, KeyCode};
405
406 let mut h = TestHarness::new().await;
407 let item_id = setup_creator_with_item(&mut h, "raceact", 1).await;
408 let key = generate_key(&mut h, &item_id).await;
409 let key_code = KeyCode::new(key["key_code"].as_str().unwrap()).unwrap();
410
411 let key_row = db::license_keys::get_license_key_by_code(&h.db, &key_code)
412 .await
413 .unwrap()
414 .expect("generated key exists");
415 let key_id = key_row.id;
416
417 // Fire many concurrent activations, each on a distinct machine.
418 let mut handles = Vec::new();
419 for i in 0..16 {
420 let pool = h.db.clone();
421 handles.push(tokio::spawn(async move {
422 db::license_keys::try_create_activation(
423 &pool,
424 key_id,
425 &format!("machine-{i}"),
426 Some("race"),
427 )
428 .await
429 .unwrap()
430 }));
431 }
432
433 let mut granted = 0;
434 for handle in handles {
435 if handle.await.unwrap().is_some() {
436 granted += 1;
437 }
438 }
439 assert_eq!(
440 granted, 1,
441 "exactly one machine wins the single seat under FOR UPDATE"
442 );
443
444 let count = db::license_keys::get_activation_count(&h.db, key_id)
445 .await
446 .unwrap();
447 assert_eq!(
448 count, 1,
449 "denormalized activation_count matches the single winner"
450 );
451 }
452
453 /// Re-activating the same machine is idempotent and never consumes extra seats,
454 /// even racing against itself, the machine_id upsert path returns the existing
455 /// row rather than allocating a new activation.
456 #[tokio::test]
457 async fn concurrent_same_machine_reactivation_is_idempotent() {
458 use makenotwork::db::{self, KeyCode};
459
460 let mut h = TestHarness::new().await;
461 let item_id = setup_creator_with_item(&mut h, "react", 1).await;
462 let key = generate_key(&mut h, &item_id).await;
463 let key_code = KeyCode::new(key["key_code"].as_str().unwrap()).unwrap();
464 let key_id = db::license_keys::get_license_key_by_code(&h.db, &key_code)
465 .await
466 .unwrap()
467 .unwrap()
468 .id;
469
470 let mut handles = Vec::new();
471 for _ in 0..8 {
472 let pool = h.db.clone();
473 handles.push(tokio::spawn(async move {
474 db::license_keys::try_create_activation(&pool, key_id, "same-machine", Some("dup"))
475 .await
476 .unwrap()
477 }));
478 }
479 for handle in handles {
480 assert!(
481 handle.await.unwrap().is_some(),
482 "same-machine re-activation always succeeds"
483 );
484 }
485
486 let count = db::license_keys::get_activation_count(&h.db, key_id)
487 .await
488 .unwrap();
489 assert_eq!(
490 count, 1,
491 "one machine == one seat regardless of concurrent re-activations"
492 );
493 }
494