//! License key workflow: create item -> enable keys -> generate key -> //! validate -> deactivate -> check status use crate::harness::TestHarness; use serde_json::Value; #[tokio::test] async fn license_key_lifecycle() { let mut h = TestHarness::new().await; // Setup: creator with project and item let user_id = h .signup("keymaker", "keymaker@example.com", "password123") .await; h.grant_creator(user_id).await; h.client.post_form("/logout", "").await; h.login("keymaker", "password123").await; let resp = h .client .post_form("/api/projects", "slug=software&title=Software") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=My+Plugin&price_cents=0&item_type=plugin", ) .await; let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); // Enable license keys with max 3 activations let resp = h .client .put_form( &format!("/api/items/{}/license-settings", item_id), "enable_license_keys=on&default_max_activations=3", ) .await; assert!( resp.status.is_success(), "Enable license keys failed: {} {}", resp.status, resp.text ); // Generate a license key let resp = h .client .post_form(&format!("/api/items/{}/keys", item_id), "") .await; assert!( resp.status.is_success(), "Generate key failed: {} {}", resp.status, resp.text ); let key: Value = resp.json(); let key_code = key["key_code"].as_str().expect("key should have key_code"); // Validate the key (public endpoint — no auth required) let resp = h .client .post_json( "/api/keys/validate", &format!( r#"{{"key": "{}", "machine_id": "machine-001", "label": "My Laptop"}}"#, key_code ), ) .await; assert!(resp.status.is_success(), "Validate key failed: {}", resp.text); let validation: Value = resp.json(); assert_eq!(validation["valid"], true, "Key should be valid"); // Check key status let resp = h .client .get(&format!("/api/keys/{}/status", key_code)) .await; assert!(resp.status.is_success(), "Key status failed: {}", resp.text); let status: Value = resp.json(); assert_eq!(status["valid"], true); assert_eq!( status["license"]["activation_count"], 1, "Should have 1 activation" ); // Deactivate let resp = h .client .post_json( "/api/keys/deactivate", &format!( r#"{{"key": "{}", "machine_id": "machine-001"}}"#, key_code ), ) .await; assert!( resp.status.is_success(), "Deactivate key failed: {}", resp.text ); let deactivation: Value = resp.json(); assert_eq!(deactivation["success"], true); // Check status again — activation count should be 0 let resp = h .client .get(&format!("/api/keys/{}/status", key_code)) .await; let status: Value = resp.json(); assert_eq!( status["license"]["activation_count"], 0, "Should have 0 activations after deactivation" ); } /// Helper: create a creator with a project and license-key-enabled item. /// Returns (item_id, key_code) after generating one key. async fn setup_creator_with_item( h: &mut TestHarness, username: &str, max_activations: u32, ) -> String { let user_id = h .signup(username, &format!("{}@example.com", username), "password123") .await; h.grant_creator(user_id).await; h.client.post_form("/logout", "").await; h.login(username, "password123").await; let resp = h .client .post_form("/api/projects", "slug=software&title=Software") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=My+Plugin&price_cents=0&item_type=plugin", ) .await; let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap().to_string(); let resp = h .client .put_form( &format!("/api/items/{}/license-settings", item_id), &format!( "enable_license_keys=on&default_max_activations={}", max_activations ), ) .await; assert!(resp.status.is_success(), "Enable license keys failed: {}", resp.text); item_id } async fn generate_key(h: &mut TestHarness, item_id: &str) -> Value { let resp = h .client .post_form(&format!("/api/items/{}/keys", item_id), "") .await; assert!(resp.status.is_success(), "Generate key failed: {}", resp.text); resp.json() } #[tokio::test] async fn max_activations_enforced() { let mut h = TestHarness::new().await; let item_id = setup_creator_with_item(&mut h, "maxact", 2).await; let key = generate_key(&mut h, &item_id).await; let key_code = key["key_code"].as_str().unwrap(); // First activation — should succeed let resp = h .client .post_json( "/api/keys/validate", &format!( r#"{{"key": "{}", "machine_id": "machine-001", "label": "Machine 1"}}"#, key_code ), ) .await; assert!(resp.status.is_success()); let v: Value = resp.json(); assert_eq!(v["valid"], true, "First activation should succeed"); // Second activation — should succeed let resp = h .client .post_json( "/api/keys/validate", &format!( r#"{{"key": "{}", "machine_id": "machine-002", "label": "Machine 2"}}"#, key_code ), ) .await; assert!(resp.status.is_success()); let v: Value = resp.json(); assert_eq!(v["valid"], true, "Second activation should succeed"); // Third activation — should fail (limit is 2) let resp = h .client .post_json( "/api/keys/validate", &format!( r#"{{"key": "{}", "machine_id": "machine-003", "label": "Machine 3"}}"#, key_code ), ) .await; let v: Value = resp.json(); assert_eq!( v["valid"], false, "Third activation should be rejected (max=2)" ); } #[tokio::test] async fn invalid_key_rejected() { let mut h = TestHarness::new().await; let resp = h .client .post_json( "/api/keys/validate", r#"{"key": "BOGUS-KEY-DOES-NOT-EXIST", "machine_id": "m1", "label": "Test"}"#, ) .await; // KeyCode deserialization rejects invalid format before reaching the handler assert!( resp.status.is_client_error(), "Bogus key should be rejected: {} {}", resp.status, resp.text ); } #[tokio::test] async fn revoke_key_then_validate_fails() { let mut h = TestHarness::new().await; let item_id = setup_creator_with_item(&mut h, "revoker", 3).await; let key = generate_key(&mut h, &item_id).await; let key_code = key["key_code"].as_str().unwrap(); // Validate first — should succeed let resp = h .client .post_json( "/api/keys/validate", &format!( r#"{{"key": "{}", "machine_id": "machine-001", "label": "Laptop"}}"#, key_code ), ) .await; assert!(resp.status.is_success()); let v: Value = resp.json(); assert_eq!(v["valid"], true); // Get the key's database ID — try the response first, fall back to DB query let key_id = if let Some(id) = key["id"].as_str() { id.to_string() } else { let row: (sqlx::types::Uuid,) = sqlx::query_as("SELECT id FROM license_keys WHERE key_code = $1") .bind(key_code) .fetch_one(&h.db) .await .expect("Key should exist in DB"); row.0.to_string() }; // Revoke the key (creator-only endpoint) let resp = h .client .post_form(&format!("/api/keys/{}/revoke", key_id), "") .await; assert!( resp.status.is_success(), "Revoke key failed: {} {}", resp.status, resp.text ); // Validate again — should fail let resp = h .client .post_json( "/api/keys/validate", &format!( r#"{{"key": "{}", "machine_id": "machine-002", "label": "Desktop"}}"#, key_code ), ) .await; let v: Value = resp.json(); assert_eq!(v["valid"], false, "Revoked key should not validate"); } #[tokio::test] async fn list_keys() { let mut h = TestHarness::new().await; let item_id = setup_creator_with_item(&mut h, "lister", 3).await; // Generate 3 keys for _ in 0..3 { generate_key(&mut h, &item_id).await; } let resp = h .client .get(&format!("/api/items/{}/keys", item_id)) .await; assert!(resp.status.is_success(), "List keys failed: {}", resp.text); let body: Value = resp.json(); let arr = body["data"].as_array().expect("Response should have a 'data' array"); assert_eq!(arr.len(), 3, "Should have 3 keys"); } #[tokio::test] async fn v1_license_endpoints() { let mut h = TestHarness::new().await; let item_id = setup_creator_with_item(&mut h, "v1user", 3).await; let key = generate_key(&mut h, &item_id).await; let key_code = key["key_code"].as_str().unwrap(); // Enable license verification on the project (required for v1 verify) let project_id: uuid::Uuid = sqlx::query_scalar( "SELECT project_id FROM items WHERE id = $1::uuid", ) .bind(&item_id) .fetch_one(&h.db) .await .unwrap(); sqlx::query("UPDATE projects SET license_verification_enabled = true WHERE id = $1") .bind(project_id) .execute(&h.db) .await .unwrap(); // POST /api/v1/license/verify let resp = h .client .post_json( "/api/v1/license/verify", &format!( r#"{{"key": "{}", "machine_fingerprint": "machine-v1"}}"#, key_code ), ) .await; assert!( resp.status.is_success(), "v1 verify failed: {} {}", resp.status, resp.text ); let v: Value = resp.json(); assert_eq!(v["valid"], true, "v1 verify should return valid=true"); // POST /api/v1/license/deactivate let resp = h .client .post_json( "/api/v1/license/deactivate", &format!( r#"{{"key": "{}", "machine_fingerprint": "machine-v1"}}"#, key_code ), ) .await; assert!( resp.status.is_success(), "v1 deactivate failed: {} {}", resp.status, resp.text ); let v: Value = resp.json(); assert_eq!(v["success"], true, "v1 deactivate should return success=true"); }