Skip to main content

max / makenotwork

13.1 KB · 440 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_eq!(resp.status, 204, "{}", resp.text);
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_eq!(resp.status, 200, "{}", resp.text);
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_eq!(resp.status, 200, "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_eq!(resp.status, 200, "{}", resp.text);
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_eq!(resp.status, 204, "{}", resp.text);
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_eq!(resp.status, 204, "Set license preset failed: {}", resp.text);
335
336 // GET license.txt should return rendered MIT text
337 let resp = h
338 .client
339 .get(&format!("/api/items/{}/license.txt", setup.item_id))
340 .await;
341 assert_eq!(resp.status, 200, "GET license.txt failed: {}", resp.text);
342 assert!(
343 resp.text.contains("MIT License"),
344 "Should contain MIT License text"
345 );
346 assert!(
347 resp.text.contains("lictxt"),
348 "Should contain owner username"
349 );
350 }
351
352 #[tokio::test]
353 async fn license_text_custom_preset() {
354 let mut h = TestHarness::new().await;
355 let setup = h.create_creator_with_item("licust", "digital", 0).await;
356
357 // Set custom license preset
358 let resp = h
359 .client
360 .put_form(
361 &format!("/api/items/{}/license-settings", setup.item_id),
362 "license_preset=custom&custom_license_text=My+custom+license+terms.",
363 )
364 .await;
365 assert_eq!(resp.status, 204, "Set custom license failed: {}", resp.text);
366
367 // GET license.txt should return custom text
368 let resp = h
369 .client
370 .get(&format!("/api/items/{}/license.txt", setup.item_id))
371 .await;
372 assert_eq!(resp.status, 200, "GET license.txt failed: {}", resp.text);
373 assert_eq!(resp.text.trim(), "My custom license terms.");
374 }
375
376 #[tokio::test]
377 async fn license_text_no_license_returns_404() {
378 let mut h = TestHarness::new().await;
379 let setup = h.create_creator_with_item("licnone", "digital", 0).await;
380
381 // No license set, should return 404
382 let resp = h
383 .client
384 .get(&format!("/api/items/{}/license.txt", setup.item_id))
385 .await;
386 assert_eq!(resp.status, 404, "Should return 404 when no license set");
387 }
388
389 #[tokio::test]
390 async fn license_text_clear_license() {
391 let mut h = TestHarness::new().await;
392 let setup = h.create_creator_with_item("licclr", "digital", 0).await;
393
394 // Set license, then clear it
395 h.client
396 .put_form(
397 &format!("/api/items/{}/license-settings", setup.item_id),
398 "license_preset=cc0",
399 )
400 .await;
401
402 // Verify it was set
403 let resp = h
404 .client
405 .get(&format!("/api/items/{}/license.txt", setup.item_id))
406 .await;
407 assert_eq!(resp.status, 200, "{}", resp.text);
408
409 // Clear the license (send empty preset)
410 h.client
411 .put_form(
412 &format!("/api/items/{}/license-settings", setup.item_id),
413 "license_preset=",
414 )
415 .await;
416
417 // Should now return 404
418 let resp = h
419 .client
420 .get(&format!("/api/items/{}/license.txt", setup.item_id))
421 .await;
422 assert_eq!(resp.status, 404, "Should return 404 after clearing license");
423 }
424
425 #[tokio::test]
426 async fn license_text_custom_without_text_fails() {
427 let mut h = TestHarness::new().await;
428 let setup = h.create_creator_with_item("licval", "digital", 0).await;
429
430 // Custom preset without text should fail validation
431 let resp = h
432 .client
433 .put_form(
434 &format!("/api/items/{}/license-settings", setup.item_id),
435 "license_preset=custom",
436 )
437 .await;
438 assert_eq!(resp.status, 422, "Should reject custom preset without text");
439 }
440