//! Content protection integration tests: //! - License verification (phone-home activation binding, JWT) //! - License deactivation (free slot) use crate::harness::TestHarness; use serde_json::Value; // License Verification (phone-home) #[tokio::test] async fn license_verify_requires_verification_enabled() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("verifymaker", "plugin", 0).await; // Enable license keys let resp = h .client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "enable_license_keys=on&default_max_activations=3", ) .await; assert!(resp.status.is_success()); // Generate a key let resp = h .client .post_form(&format!("/api/items/{}/keys", setup.item_id), "") .await; let key: Value = resp.json(); let key_code = key["key_code"].as_str().unwrap(); // Try to verify, should fail because project doesn't have verification enabled let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-aaa"}}"#), ) .await; assert!(resp.status.is_success()); let body: Value = resp.json(); assert_eq!(body["valid"], false); assert_eq!(body["error"], "verification_not_enabled"); } #[tokio::test] async fn license_verify_lifecycle() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("verifylc", "plugin", 0).await; // Enable license keys h.client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "enable_license_keys=on&default_max_activations=3", ) .await; // Enable license verification on the project sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid") .bind(&setup.project_id) .execute(&h.db) .await .unwrap(); // Generate a key let resp = h .client .post_form(&format!("/api/items/{}/keys", setup.item_id), "") .await; let key: Value = resp.json(); let key_code = key["key_code"].as_str().unwrap(); // Verify, should succeed and return JWT let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-001"}}"#), ) .await; assert!(resp.status.is_success(), "Verify failed: {}", resp.text); let body: Value = resp.json(); assert_eq!(body["valid"], true, "Key should be valid"); assert!(body["token"].is_string(), "Should return a JWT token"); assert!( body["expires_in"].as_i64().unwrap() > 0, "Should have expiry" ); // Re-verify same machine, should succeed (idempotent) let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-001"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], true, "Re-verify should succeed"); // Verify on second machine let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-002"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], true, "Second machine should work"); // Verify on third machine let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-003"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], true, "Third machine should work (max=3)"); // Fourth machine, should hit activation cap let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-004"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], false, "Fourth machine should be rejected"); assert_eq!(body["error"], "activation_limit_reached"); } #[tokio::test] async fn license_deactivate_frees_slot() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("deactlc", "plugin", 0).await; // Enable license keys + verification h.client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "enable_license_keys=on&default_max_activations=2", ) .await; sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid") .bind(&setup.project_id) .execute(&h.db) .await .unwrap(); // Generate key let resp = h .client .post_form(&format!("/api/items/{}/keys", setup.item_id), "") .await; let key: Value = resp.json(); let key_code = key["key_code"].as_str().unwrap(); // Activate on 2 machines (max) for m in &["m-1", "m-2"] { let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "{m}"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], true); } // Third machine should fail let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "m-3"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], false); // Deactivate machine 1 let resp = h .client .post_json( "/api/v1/license/deactivate", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "m-1"}}"#), ) .await; assert!(resp.status.is_success()); let body: Value = resp.json(); assert_eq!(body["success"], true); // Third machine should now work (slot freed) let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "m-3"}}"#), ) .await; let body: Value = resp.json(); assert_eq!( body["valid"], true, "Should work after deactivation freed a slot" ); } #[tokio::test] async fn license_verify_revoked_key() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("revokelc", "plugin", 0).await; // Enable license keys + verification h.client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "enable_license_keys=on&default_max_activations=3", ) .await; sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid") .bind(&setup.project_id) .execute(&h.db) .await .unwrap(); // Generate key let resp = h .client .post_form(&format!("/api/items/{}/keys", setup.item_id), "") .await; let key: Value = resp.json(); let key_code = key["key_code"].as_str().unwrap(); let key_id = key["id"].as_str().unwrap(); // Revoke the key let resp = h .client .post_form(&format!("/api/keys/{key_id}/revoke"), "") .await; assert!(resp.status.is_success() || resp.status == 204); // Verify should fail with key_revoked let resp = h .client .post_json( "/api/v1/license/verify", &format!(r#"{{"key": "{key_code}", "machine_fingerprint": "machine-x"}}"#), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], false); assert_eq!(body["error"], "key_revoked"); // A revoked key must never bind a machine activation (Run #23: the in-lock // revoked_at re-check in try_create_activation backstops the verify fast-path). let activations: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM license_activations WHERE license_key_id = $1::uuid", ) .bind(key_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!( activations, 0, "a revoked key must not create an activation" ); } // Project license_verification_enabled flag #[tokio::test] async fn project_license_verification_flag() { let mut h = TestHarness::new().await; let _user_id = h.create_creator("flagmaker").await; let resp = h .client .post_form("/api/projects", "slug=flagproj&title=FlagProject") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); // Default should be false let enabled: bool = sqlx::query_scalar("SELECT license_verification_enabled FROM projects WHERE id = $1::uuid") .bind(project_id) .fetch_one(&h.db) .await .unwrap(); assert!(!enabled, "Should default to false"); // Enable it sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1::uuid") .bind(project_id) .execute(&h.db) .await .unwrap(); let enabled: bool = sqlx::query_scalar("SELECT license_verification_enabled FROM projects WHERE id = $1::uuid") .bind(project_id) .fetch_one(&h.db) .await .unwrap(); assert!(enabled, "Should be true after update"); } // License Text #[tokio::test] async fn license_text_set_and_serve_preset() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("lictxt", "digital", 0).await; // Set license preset via license-settings endpoint let resp = h .client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "license_preset=mit", ) .await; assert!( resp.status.is_success(), "Set license preset failed: {}", resp.text ); // GET license.txt should return rendered MIT text let resp = h .client .get(&format!("/api/items/{}/license.txt", setup.item_id)) .await; assert!( resp.status.is_success(), "GET license.txt failed: {}", resp.text ); assert!( resp.text.contains("MIT License"), "Should contain MIT License text" ); assert!( resp.text.contains("lictxt"), "Should contain owner username" ); } #[tokio::test] async fn license_text_custom_preset() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("licust", "digital", 0).await; // Set custom license preset let resp = h .client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "license_preset=custom&custom_license_text=My+custom+license+terms.", ) .await; assert!( resp.status.is_success(), "Set custom license failed: {}", resp.text ); // GET license.txt should return custom text let resp = h .client .get(&format!("/api/items/{}/license.txt", setup.item_id)) .await; assert!( resp.status.is_success(), "GET license.txt failed: {}", resp.text ); assert_eq!(resp.text.trim(), "My custom license terms."); } #[tokio::test] async fn license_text_no_license_returns_404() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("licnone", "digital", 0).await; // No license set, should return 404 let resp = h .client .get(&format!("/api/items/{}/license.txt", setup.item_id)) .await; assert_eq!(resp.status, 404, "Should return 404 when no license set"); } #[tokio::test] async fn license_text_clear_license() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("licclr", "digital", 0).await; // Set license, then clear it h.client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "license_preset=cc0", ) .await; // Verify it was set let resp = h .client .get(&format!("/api/items/{}/license.txt", setup.item_id)) .await; assert!(resp.status.is_success()); // Clear the license (send empty preset) h.client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "license_preset=", ) .await; // Should now return 404 let resp = h .client .get(&format!("/api/items/{}/license.txt", setup.item_id)) .await; assert_eq!(resp.status, 404, "Should return 404 after clearing license"); } #[tokio::test] async fn license_text_custom_without_text_fails() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("licval", "digital", 0).await; // Custom preset without text should fail validation let resp = h .client .put_form( &format!("/api/items/{}/license-settings", setup.item_id), "license_preset=custom", ) .await; assert!( !resp.status.is_success(), "Should reject custom preset without text" ); }