//! 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": "{}", "machine_fingerprint": "machine-aaa"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "machine-001"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "machine-001"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "machine-002"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "machine-003"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "machine-004"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "{}"}}"#, key_code, 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": "{}", "machine_fingerprint": "m-3"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "m-1"}}"#, key_code ), ) .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": "{}", "machine_fingerprint": "m-3"}}"#, key_code ), ) .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/{}/revoke", key_id), "") .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": "{}", "machine_fingerprint": "machine-x"}}"#, key_code ), ) .await; let body: Value = resp.json(); assert_eq!(body["valid"], false); assert_eq!(body["error"], "key_revoked"); } // ============================================================================= // 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"); }