Skip to main content

max / makenotwork

13.2 KB · 459 lines History Blame Raw
1 //! Content protection integration tests:
2 //! - License verification (phone-home activation binding, JWT)
3 //! - License deactivation (free slot)
4
5 use crate::harness::TestHarness;
6 use serde_json::Value;
7
8 // License Verification (phone-home)
9
10 #[tokio::test]
11 async fn license_verify_requires_verification_enabled() {
12 let mut h = TestHarness::new().await;
13
14 let setup = h.create_creator_with_item("verifymaker", "plugin", 0).await;
15
16 // Enable license keys
17 let resp = h
18 .client
19 .put_form(
20 &format!("/api/items/{}/license-settings", setup.item_id),
21 "enable_license_keys=on&default_max_activations=3",
22 )
23 .await;
24 assert!(resp.status.is_success());
25
26 // Generate a key
27 let resp = h
28 .client
29 .post_form(&format!("/api/items/{}/keys", setup.item_id), "")
30 .await;
31 let key: Value = resp.json();
32 let key_code = key["key_code"].as_str().unwrap();
33
34 // Try to verify, should fail because project doesn't have verification enabled
35 let resp = h
36 .client
37 .post_json(
38 "/api/v1/license/verify",
39 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-aaa"}}"#),
40 )
41 .await;
42 assert!(resp.status.is_success());
43 let body: Value = resp.json();
44 assert_eq!(body["valid"], false);
45 assert_eq!(body["error"], "verification_not_enabled");
46 }
47
48 #[tokio::test]
49 async fn license_verify_lifecycle() {
50 let mut h = TestHarness::new().await;
51
52 let setup = h.create_creator_with_item("verifylc", "plugin", 0).await;
53
54 // Enable license keys
55 h.client
56 .put_form(
57 &format!("/api/items/{}/license-settings", setup.item_id),
58 "enable_license_keys=on&default_max_activations=3",
59 )
60 .await;
61
62 // Enable license verification on the project
63 sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid")
64 .bind(&setup.project_id)
65 .execute(&h.db)
66 .await
67 .unwrap();
68
69 // Generate a key
70 let resp = h
71 .client
72 .post_form(&format!("/api/items/{}/keys", setup.item_id), "")
73 .await;
74 let key: Value = resp.json();
75 let key_code = key["key_code"].as_str().unwrap();
76
77 // Verify, should succeed and return JWT
78 let resp = h
79 .client
80 .post_json(
81 "/api/v1/license/verify",
82 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-001"}}"#),
83 )
84 .await;
85 assert!(resp.status.is_success(), "Verify failed: {}", resp.text);
86 let body: Value = resp.json();
87 assert_eq!(body["valid"], true, "Key should be valid");
88 assert!(body["token"].is_string(), "Should return a JWT token");
89 assert!(
90 body["expires_in"].as_i64().unwrap() > 0,
91 "Should have expiry"
92 );
93
94 // Re-verify same machine, should succeed (idempotent)
95 let resp = h
96 .client
97 .post_json(
98 "/api/v1/license/verify",
99 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-001"}}"#),
100 )
101 .await;
102 let body: Value = resp.json();
103 assert_eq!(body["valid"], true, "Re-verify should succeed");
104
105 // Verify on second machine
106 let resp = h
107 .client
108 .post_json(
109 "/api/v1/license/verify",
110 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-002"}}"#),
111 )
112 .await;
113 let body: Value = resp.json();
114 assert_eq!(body["valid"], true, "Second machine should work");
115
116 // Verify on third machine
117 let resp = h
118 .client
119 .post_json(
120 "/api/v1/license/verify",
121 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-003"}}"#),
122 )
123 .await;
124 let body: Value = resp.json();
125 assert_eq!(body["valid"], true, "Third machine should work (max=3)");
126
127 // Fourth machine, should hit activation cap
128 let resp = h
129 .client
130 .post_json(
131 "/api/v1/license/verify",
132 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-004"}}"#),
133 )
134 .await;
135 let body: Value = resp.json();
136 assert_eq!(body["valid"], false, "Fourth machine should be rejected");
137 assert_eq!(body["error"], "activation_limit_reached");
138 }
139
140 #[tokio::test]
141 async fn license_deactivate_frees_slot() {
142 let mut h = TestHarness::new().await;
143
144 let setup = h.create_creator_with_item("deactlc", "plugin", 0).await;
145
146 // Enable license keys + verification
147 h.client
148 .put_form(
149 &format!("/api/items/{}/license-settings", setup.item_id),
150 "enable_license_keys=on&default_max_activations=2",
151 )
152 .await;
153 sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid")
154 .bind(&setup.project_id)
155 .execute(&h.db)
156 .await
157 .unwrap();
158
159 // Generate key
160 let resp = h
161 .client
162 .post_form(&format!("/api/items/{}/keys", setup.item_id), "")
163 .await;
164 let key: Value = resp.json();
165 let key_code = key["key_code"].as_str().unwrap();
166
167 // Activate on 2 machines (max)
168 for m in &["m-1", "m-2"] {
169 let resp = h
170 .client
171 .post_json(
172 "/api/v1/license/verify",
173 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "{m}"}}"#),
174 )
175 .await;
176 let body: Value = resp.json();
177 assert_eq!(body["valid"], true);
178 }
179
180 // Third machine should fail
181 let resp = h
182 .client
183 .post_json(
184 "/api/v1/license/verify",
185 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "m-3"}}"#),
186 )
187 .await;
188 let body: Value = resp.json();
189 assert_eq!(body["valid"], false);
190
191 // Deactivate machine 1
192 let resp = h
193 .client
194 .post_json(
195 "/api/v1/license/deactivate",
196 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "m-1"}}"#),
197 )
198 .await;
199 assert!(resp.status.is_success());
200 let body: Value = resp.json();
201 assert_eq!(body["success"], true);
202
203 // Third machine should now work (slot freed)
204 let resp = h
205 .client
206 .post_json(
207 "/api/v1/license/verify",
208 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "m-3"}}"#),
209 )
210 .await;
211 let body: Value = resp.json();
212 assert_eq!(
213 body["valid"], true,
214 "Should work after deactivation freed a slot"
215 );
216 }
217
218 #[tokio::test]
219 async fn license_verify_revoked_key() {
220 let mut h = TestHarness::new().await;
221
222 let setup = h.create_creator_with_item("revokelc", "plugin", 0).await;
223
224 // Enable license keys + verification
225 h.client
226 .put_form(
227 &format!("/api/items/{}/license-settings", setup.item_id),
228 "enable_license_keys=on&default_max_activations=3",
229 )
230 .await;
231 sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid")
232 .bind(&setup.project_id)
233 .execute(&h.db)
234 .await
235 .unwrap();
236
237 // Generate key
238 let resp = h
239 .client
240 .post_form(&format!("/api/items/{}/keys", setup.item_id), "")
241 .await;
242 let key: Value = resp.json();
243 let key_code = key["key_code"].as_str().unwrap();
244 let key_id = key["id"].as_str().unwrap();
245
246 // Revoke the key
247 let resp = h
248 .client
249 .post_form(&format!("/api/keys/{key_id}/revoke"), "")
250 .await;
251 assert!(resp.status.is_success() || resp.status == 204);
252
253 // Verify should fail with key_revoked
254 let resp = h
255 .client
256 .post_json(
257 "/api/v1/license/verify",
258 &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-x"}}"#),
259 )
260 .await;
261 let body: Value = resp.json();
262 assert_eq!(body["valid"], false);
263 assert_eq!(body["error"], "key_revoked");
264
265 // A revoked key must never bind a machine activation (Run #23: the in-lock
266 // revoked_at re-check in try_create_activation backstops the verify fast-path).
267 let activations: i64 = sqlx::query_scalar(
268 "SELECT COUNT(*) FROM license_activations WHERE license_key_id = $1::uuid",
269 )
270 .bind(key_id)
271 .fetch_one(&h.db)
272 .await
273 .unwrap();
274 assert_eq!(
275 activations, 0,
276 "a revoked key must not create an activation"
277 );
278 }
279
280 // Project license_verification_enabled flag
281
282 #[tokio::test]
283 async fn project_license_verification_flag() {
284 let mut h = TestHarness::new().await;
285 let _user_id = h.create_creator("flagmaker").await;
286
287 let resp = h
288 .client
289 .post_form("/api/projects", "slug=flagproj&title=FlagProject")
290 .await;
291 let project: Value = resp.json();
292 let project_id = project["id"].as_str().unwrap();
293
294 // Default should be false
295 let enabled: bool =
296 sqlx::query_scalar("SELECT license_verification_enabled FROM projects WHERE id = $1::uuid")
297 .bind(project_id)
298 .fetch_one(&h.db)
299 .await
300 .unwrap();
301 assert!(!enabled, "Should default to false");
302
303 // Enable it
304 sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid")
305 .bind(project_id)
306 .execute(&h.db)
307 .await
308 .unwrap();
309
310 let enabled: bool =
311 sqlx::query_scalar("SELECT license_verification_enabled FROM projects WHERE id = $1::uuid")
312 .bind(project_id)
313 .fetch_one(&h.db)
314 .await
315 .unwrap();
316 assert!(enabled, "Should be true after update");
317 }
318
319 // License Text
320
321 #[tokio::test]
322 async fn license_text_set_and_serve_preset() {
323 let mut h = TestHarness::new().await;
324 let setup = h.create_creator_with_item("lictxt", "digital", 0).await;
325
326 // Set license preset via license-settings endpoint
327 let resp = h
328 .client
329 .put_form(
330 &format!("/api/items/{}/license-settings", setup.item_id),
331 "license_preset=mit",
332 )
333 .await;
334 assert!(
335 resp.status.is_success(),
336 "Set license preset failed: {}",
337 resp.text
338 );
339
340 // GET license.txt should return rendered MIT text
341 let resp = h
342 .client
343 .get(&format!("/api/items/{}/license.txt", setup.item_id))
344 .await;
345 assert!(
346 resp.status.is_success(),
347 "GET license.txt failed: {}",
348 resp.text
349 );
350 assert!(
351 resp.text.contains("MIT License"),
352 "Should contain MIT License text"
353 );
354 assert!(
355 resp.text.contains("lictxt"),
356 "Should contain owner username"
357 );
358 }
359
360 #[tokio::test]
361 async fn license_text_custom_preset() {
362 let mut h = TestHarness::new().await;
363 let setup = h.create_creator_with_item("licust", "digital", 0).await;
364
365 // Set custom license preset
366 let resp = h
367 .client
368 .put_form(
369 &format!("/api/items/{}/license-settings", setup.item_id),
370 "license_preset=custom&custom_license_text=My+custom+license+terms.",
371 )
372 .await;
373 assert!(
374 resp.status.is_success(),
375 "Set custom license failed: {}",
376 resp.text
377 );
378
379 // GET license.txt should return custom text
380 let resp = h
381 .client
382 .get(&format!("/api/items/{}/license.txt", setup.item_id))
383 .await;
384 assert!(
385 resp.status.is_success(),
386 "GET license.txt failed: {}",
387 resp.text
388 );
389 assert_eq!(resp.text.trim(), "My custom license terms.");
390 }
391
392 #[tokio::test]
393 async fn license_text_no_license_returns_404() {
394 let mut h = TestHarness::new().await;
395 let setup = h.create_creator_with_item("licnone", "digital", 0).await;
396
397 // No license set, should return 404
398 let resp = h
399 .client
400 .get(&format!("/api/items/{}/license.txt", setup.item_id))
401 .await;
402 assert_eq!(resp.status, 404, "Should return 404 when no license set");
403 }
404
405 #[tokio::test]
406 async fn license_text_clear_license() {
407 let mut h = TestHarness::new().await;
408 let setup = h.create_creator_with_item("licclr", "digital", 0).await;
409
410 // Set license, then clear it
411 h.client
412 .put_form(
413 &format!("/api/items/{}/license-settings", setup.item_id),
414 "license_preset=cc0",
415 )
416 .await;
417
418 // Verify it was set
419 let resp = h
420 .client
421 .get(&format!("/api/items/{}/license.txt", setup.item_id))
422 .await;
423 assert!(resp.status.is_success());
424
425 // Clear the license (send empty preset)
426 h.client
427 .put_form(
428 &format!("/api/items/{}/license-settings", setup.item_id),
429 "license_preset=",
430 )
431 .await;
432
433 // Should now return 404
434 let resp = h
435 .client
436 .get(&format!("/api/items/{}/license.txt", setup.item_id))
437 .await;
438 assert_eq!(resp.status, 404, "Should return 404 after clearing license");
439 }
440
441 #[tokio::test]
442 async fn license_text_custom_without_text_fails() {
443 let mut h = TestHarness::new().await;
444 let setup = h.create_creator_with_item("licval", "digital", 0).await;
445
446 // Custom preset without text should fail validation
447 let resp = h
448 .client
449 .put_form(
450 &format!("/api/items/{}/license-settings", setup.item_id),
451 "license_preset=custom",
452 )
453 .await;
454 assert!(
455 !resp.status.is_success(),
456 "Should reject custom preset without text"
457 );
458 }
459