Skip to main content

keygen_rs/
license.rs

1//! License management and validation.
2//!
3//! This module provides functionality for validating, verifying, and managing licenses.
4//! It supports both online validation against the Keygen API and offline verification
5//! of signed license keys.
6
7use std::env;
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use std::collections::HashMap;
13
14use crate::certificate::CertificateFileResponse;
15use crate::client::{Client, ClientOptions};
16use crate::component::Component;
17use crate::config::{get_config, KeygenConfig};
18use crate::entitlement::{Entitlement, EntitlementsResponse};
19use crate::errors::Error;
20use crate::insert_optional;
21use crate::license_file::LicenseFile;
22use crate::machine::{Machine, MachineResponse, MachinesResponse};
23#[cfg(feature = "token")]
24use crate::token::{token_request_attributes, CreateTokenRequest, Token, TokenResponse};
25#[cfg(feature = "token")]
26use crate::user::{User, UserAttributes};
27use crate::verifier::Verifier;
28use crate::KeygenResponseData;
29use std::sync::Arc;
30
31/// Represents an optional field update in API requests.
32///
33/// This enum provides explicit semantics for nullable field updates:
34/// - `Keep`: Do not include this field in the update (no change)
35/// - `Clear`: Set the field to null/None
36/// - `Set(T)`: Set the field to a specific value
37#[derive(Debug, Clone, Default)]
38pub enum UpdateField<T> {
39    /// Do not update this field
40    #[default]
41    Keep,
42    /// Clear this field (set to null)
43    Clear,
44    /// Set this field to a specific value
45    Set(T),
46}
47
48impl<T: Serialize> UpdateField<T> {
49    /// Applies this update field to a JSON map if it should be included
50    pub fn apply_to(&self, map: &mut serde_json::Map<String, Value>, key: &str) {
51        match self {
52            UpdateField::Keep => {}
53            UpdateField::Clear => {
54                map.insert(key.to_string(), Value::Null);
55            }
56            UpdateField::Set(value) => {
57                map.insert(key.to_string(), json!(value));
58            }
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub enum SchemeCode {
65    #[serde(rename = "ED25519_SIGN")]
66    Ed25519Sign,
67    #[serde(rename = "ECDSA_P256_SIGN")]
68    EcdsaP256Sign,
69    #[serde(rename = "RSA_2048_PKCS1_PSS_SIGN_V2")]
70    Rsa2048Pkcs1PssSignV2,
71    #[serde(rename = "RSA_2048_PKCS1_SIGN_V2")]
72    Rsa2048Pkcs1SignV2,
73    #[serde(rename = "RSA_2048_PKCS1_ENCRYPT")]
74    Rsa2048Pkcs1Encrypt,
75    #[serde(rename = "RSA_2048_JWT_RS256")]
76    Rsa2048JwtRs256,
77    #[serde(rename = "LEGACY_ENCRYPT")]
78    LegacyEncrypt,
79    #[serde(rename = "RSA_2048_PKCS1_PSS_SIGN")]
80    Rsa2048Pkcs1PssSign, // Deprecated
81    #[serde(rename = "RSA_2048_PKCS1_SIGN")]
82    Rsa2048Pkcs1Sign, // Deprecated
83}
84
85/// License status as returned by the Keygen API
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
88pub enum LicenseStatus {
89    Active,
90    Inactive,
91    Expiring,
92    Expired,
93    Suspended,
94    Banned,
95}
96
97impl LicenseStatus {
98    /// Parses a LicenseStatus from a string, returning None for unknown values
99    pub fn parse(s: &str) -> Option<Self> {
100        match s.to_uppercase().as_str() {
101            "ACTIVE" => Some(Self::Active),
102            "INACTIVE" => Some(Self::Inactive),
103            "EXPIRING" => Some(Self::Expiring),
104            "EXPIRED" => Some(Self::Expired),
105            "SUSPENDED" => Some(Self::Suspended),
106            "BANNED" => Some(Self::Banned),
107            _ => None,
108        }
109    }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub(crate) struct LicenseResponse<M> {
114    pub meta: Option<M>,
115    pub data: KeygenResponseData<LicenseAttributes>,
116}
117
118#[cfg(feature = "token")]
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub(crate) struct LicenseUsersResponse {
121    pub data: Vec<KeygenResponseData<UserAttributes>>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub(crate) struct ValidationMeta {
126    pub ts: DateTime<Utc>,
127    pub valid: bool,
128    pub detail: String,
129    pub code: String,
130    pub scope: ValidationScope,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub(crate) struct ValidationScope {
135    pub fingerprint: Option<String>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub(crate) struct LicenseAttributes {
140    pub key: String,
141    pub name: Option<String>,
142    pub expiry: Option<DateTime<Utc>>,
143    pub status: Option<String>,
144    pub uses: Option<i32>,
145    #[serde(rename = "maxMachines")]
146    pub max_machines: Option<i32>,
147    #[serde(rename = "maxCores")]
148    pub max_cores: Option<i32>,
149    #[serde(rename = "maxUses")]
150    pub max_uses: Option<i32>,
151    #[serde(rename = "maxProcesses")]
152    pub max_processes: Option<i32>,
153    #[serde(rename = "maxUsers")]
154    pub max_users: Option<i32>,
155    pub protected: Option<bool>,
156    pub suspended: Option<bool>,
157    pub permissions: Option<Vec<String>>,
158    pub metadata: HashMap<String, Value>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct License {
163    pub id: String,
164    #[serde(skip_serializing)]
165    pub scheme: Option<SchemeCode>,
166    pub key: String,
167    pub name: Option<String>,
168    pub expiry: Option<DateTime<Utc>>,
169    pub status: Option<String>,
170    pub uses: Option<i32>,
171    pub max_machines: Option<i32>,
172    pub max_cores: Option<i32>,
173    pub max_uses: Option<i32>,
174    pub max_processes: Option<i32>,
175    pub max_users: Option<i32>,
176    pub protected: Option<bool>,
177    pub suspended: Option<bool>,
178    pub permissions: Option<Vec<String>>,
179    pub policy: Option<String>,
180    pub metadata: HashMap<String, Value>,
181    pub account_id: Option<String>,
182    pub product_id: Option<String>,
183    pub group_id: Option<String>,
184    pub owner_id: Option<String>,
185    #[serde(skip)]
186    pub config: Option<Arc<KeygenConfig>>,
187}
188
189#[derive(Debug, Clone, Default)]
190pub struct LicenseCheckoutOpts {
191    pub ttl: Option<i64>,
192    pub include: Option<Vec<String>>,
193}
194
195impl LicenseCheckoutOpts {
196    /// Create new checkout options with default settings
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Create checkout options with TTL
202    pub fn with_ttl(ttl: i64) -> Self {
203        Self {
204            ttl: Some(ttl),
205            ..Self::default()
206        }
207    }
208
209    /// Create checkout options with specific relationships to include
210    pub fn with_include(include: Vec<String>) -> Self {
211        Self {
212            include: Some(include),
213            ..Self::default()
214        }
215    }
216}
217
218#[derive(Debug, Default, Serialize)]
219pub struct PaginationOptions {
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub limit: Option<i32>,
222    #[serde(rename = "page[number]", skip_serializing_if = "Option::is_none")]
223    pub page_number: Option<i32>,
224    #[serde(rename = "page[size]", skip_serializing_if = "Option::is_none")]
225    pub page_size: Option<i32>,
226}
227
228#[derive(Debug, Clone, Default)]
229pub struct NumericFilter {
230    pub eq: Option<i32>,
231    pub gt: Option<i32>,
232    pub gte: Option<i32>,
233    pub lt: Option<i32>,
234    pub lte: Option<i32>,
235}
236
237#[derive(Debug, Clone, Default)]
238pub struct DateWindowFilter {
239    pub r#in: Option<String>,
240    pub on: Option<String>,
241    pub before: Option<String>,
242    pub after: Option<String>,
243}
244
245#[derive(Debug, Clone, Default)]
246pub struct LicenseActivityFilter {
247    pub inside: Option<String>,
248    pub outside: Option<String>,
249    pub before: Option<String>,
250    pub after: Option<String>,
251}
252
253/// Simple license list options with common filters
254#[derive(Debug, Default)]
255pub struct LicenseListOptions {
256    // Pagination - following Keygen API standards
257    pub limit: Option<i32>, // Number of resources to return (1-100, default 10)
258    pub page_number: Option<i32>, // Page number to retrieve
259    pub page_size: Option<i32>, // Number of resources per page (1-100)
260
261    // Common filters
262    pub status: Option<String>,  // "ACTIVE", "EXPIRED", "SUSPENDED", etc.
263    pub product: Option<String>, // Product ID
264    pub policy: Option<String>,  // Policy ID
265    pub owner: Option<String>,   // Owner ID or email
266    pub user: Option<String>,    // User ID or email
267    pub group: Option<String>,
268    pub machine: Option<String>,
269    pub assigned: Option<bool>,
270    pub unassigned: Option<bool>,
271    pub activated: Option<bool>,
272    pub metadata: Option<HashMap<String, Value>>,
273    pub activations: Option<NumericFilter>,
274    pub expires: Option<DateWindowFilter>,
275    pub expired: Option<DateWindowFilter>,
276    pub activity: Option<LicenseActivityFilter>,
277}
278
279/// Request structure for creating a new license with complete API support
280#[derive(Debug, Default)]
281pub struct LicenseCreateRequest {
282    // Required relationship
283    pub policy_id: String,
284
285    // Optional attributes
286    pub name: Option<String>,
287    pub key: Option<String>,
288    pub expiry: Option<DateTime<Utc>>,
289    pub max_machines: Option<i32>,
290    pub max_processes: Option<i32>,
291    pub max_users: Option<i32>,
292    pub max_cores: Option<i32>,
293    pub max_uses: Option<i32>,
294    pub protected: Option<bool>,
295    pub suspended: Option<bool>,
296    pub permissions: Option<Vec<String>>,
297    pub metadata: Option<HashMap<String, Value>>,
298
299    // Optional relationships
300    pub owner_id: Option<String>, // User ID
301    pub group_id: Option<String>, // Group ID
302}
303
304/// Request structure for updating a license with complete API support
305#[derive(Debug, Default)]
306pub struct LicenseUpdateRequest {
307    // All optional attributes that can be updated
308    pub name: Option<String>,
309    pub expiry: Option<DateTime<Utc>>,
310    pub max_machines: UpdateField<i32>,
311    pub max_processes: UpdateField<i32>,
312    pub max_users: UpdateField<i32>,
313    pub max_cores: UpdateField<i32>,
314    pub max_uses: UpdateField<i32>,
315    pub protected: Option<bool>,
316    pub suspended: Option<bool>,
317    pub permissions: Option<Vec<String>>,
318    pub metadata: Option<HashMap<String, Value>>,
319}
320
321impl LicenseCreateRequest {
322    /// Create a new license creation request with the required policy ID
323    pub fn new(policy_id: String) -> Self {
324        Self {
325            policy_id,
326            ..Default::default()
327        }
328    }
329
330    /// Set the license name
331    pub fn with_name(mut self, name: String) -> Self {
332        self.name = Some(name);
333        self
334    }
335
336    /// Set a custom license key
337    pub fn with_key(mut self, key: String) -> Self {
338        self.key = Some(key);
339        self
340    }
341
342    /// Set the expiry date
343    pub fn with_expiry(mut self, expiry: DateTime<Utc>) -> Self {
344        self.expiry = Some(expiry);
345        self
346    }
347
348    /// Set the maximum number of machines
349    pub fn with_max_machines(mut self, max_machines: i32) -> Self {
350        self.max_machines = Some(max_machines);
351        self
352    }
353
354    /// Set the maximum number of processes
355    pub fn with_max_processes(mut self, max_processes: i32) -> Self {
356        self.max_processes = Some(max_processes);
357        self
358    }
359
360    /// Set the maximum number of users
361    pub fn with_max_users(mut self, max_users: i32) -> Self {
362        self.max_users = Some(max_users);
363        self
364    }
365
366    /// Set the maximum number of cores
367    pub fn with_max_cores(mut self, max_cores: i32) -> Self {
368        self.max_cores = Some(max_cores);
369        self
370    }
371
372    /// Set the maximum number of uses
373    pub fn with_max_uses(mut self, max_uses: i32) -> Self {
374        self.max_uses = Some(max_uses);
375        self
376    }
377
378    /// Set the protected flag
379    pub fn with_protected(mut self, protected: bool) -> Self {
380        self.protected = Some(protected);
381        self
382    }
383
384    /// Set the suspended flag
385    pub fn with_suspended(mut self, suspended: bool) -> Self {
386        self.suspended = Some(suspended);
387        self
388    }
389
390    /// Set the permissions array
391    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
392        self.permissions = Some(permissions);
393        self
394    }
395
396    /// Set the metadata
397    pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
398        self.metadata = Some(metadata);
399        self
400    }
401
402    /// Set the owner (user) ID
403    pub fn with_owner_id(mut self, owner_id: String) -> Self {
404        self.owner_id = Some(owner_id);
405        self
406    }
407
408    /// Set the group ID
409    pub fn with_group_id(mut self, group_id: String) -> Self {
410        self.group_id = Some(group_id);
411        self
412    }
413
414    /// Convert this request to attributes and relationships JSON maps for the API
415    pub fn to_json_body(self) -> Value {
416        let mut attributes = serde_json::Map::new();
417        let mut relationships = serde_json::Map::new();
418
419        // Build attributes — all types here (String, i32, DateTime, bool, Vec, HashMap)
420        // are infallibly serializable, so unwrap is safe.
421        let _ = insert_optional(&mut attributes, "name", self.name);
422        let _ = insert_optional(&mut attributes, "key", self.key);
423        let _ = insert_optional(&mut attributes, "expiry", self.expiry);
424        let _ = insert_optional(&mut attributes, "maxMachines", self.max_machines);
425        let _ = insert_optional(&mut attributes, "maxProcesses", self.max_processes);
426        let _ = insert_optional(&mut attributes, "maxUsers", self.max_users);
427        let _ = insert_optional(&mut attributes, "maxCores", self.max_cores);
428        let _ = insert_optional(&mut attributes, "maxUses", self.max_uses);
429        let _ = insert_optional(&mut attributes, "protected", self.protected);
430        let _ = insert_optional(&mut attributes, "suspended", self.suspended);
431        let _ = insert_optional(&mut attributes, "permissions", self.permissions);
432        let _ = insert_optional(&mut attributes, "metadata", self.metadata);
433
434        // Build relationships - policy is required
435        relationships.insert(
436            "policy".to_string(),
437            json!({
438                "data": {
439                    "type": "policies",
440                    "id": self.policy_id
441                }
442            }),
443        );
444
445        if let Some(owner_id) = self.owner_id {
446            relationships.insert(
447                "owner".to_string(),
448                json!({
449                    "data": {
450                        "type": "users",
451                        "id": owner_id
452                    }
453                }),
454            );
455        }
456
457        if let Some(group_id) = self.group_id {
458            relationships.insert(
459                "group".to_string(),
460                json!({
461                    "data": {
462                        "type": "groups",
463                        "id": group_id
464                    }
465                }),
466            );
467        }
468
469        json!({
470            "data": {
471                "type": "licenses",
472                "attributes": attributes,
473                "relationships": relationships
474            }
475        })
476    }
477}
478
479impl LicenseUpdateRequest {
480    /// Create a new empty license update request
481    pub fn new() -> Self {
482        Self::default()
483    }
484
485    /// Set the license name
486    pub fn with_name(mut self, name: String) -> Self {
487        self.name = Some(name);
488        self
489    }
490
491    /// Set the expiry date
492    pub fn with_expiry(mut self, expiry: DateTime<Utc>) -> Self {
493        self.expiry = Some(expiry);
494        self
495    }
496
497    /// Set the maximum number of machines
498    pub fn with_max_machines(mut self, max_machines: i32) -> Self {
499        self.max_machines = UpdateField::Set(max_machines);
500        self
501    }
502
503    /// Clear the maximum number of machines (set to null)
504    pub fn clear_max_machines(mut self) -> Self {
505        self.max_machines = UpdateField::Clear;
506        self
507    }
508
509    /// Set the maximum number of processes
510    pub fn with_max_processes(mut self, max_processes: i32) -> Self {
511        self.max_processes = UpdateField::Set(max_processes);
512        self
513    }
514
515    /// Clear the maximum number of processes (set to null)
516    pub fn clear_max_processes(mut self) -> Self {
517        self.max_processes = UpdateField::Clear;
518        self
519    }
520
521    /// Set the maximum number of users
522    pub fn with_max_users(mut self, max_users: i32) -> Self {
523        self.max_users = UpdateField::Set(max_users);
524        self
525    }
526
527    /// Clear the maximum number of users (set to null)
528    pub fn clear_max_users(mut self) -> Self {
529        self.max_users = UpdateField::Clear;
530        self
531    }
532
533    /// Set the maximum number of cores
534    pub fn with_max_cores(mut self, max_cores: i32) -> Self {
535        self.max_cores = UpdateField::Set(max_cores);
536        self
537    }
538
539    /// Clear the maximum number of cores (set to null)
540    pub fn clear_max_cores(mut self) -> Self {
541        self.max_cores = UpdateField::Clear;
542        self
543    }
544
545    /// Set the maximum number of uses
546    pub fn with_max_uses(mut self, max_uses: i32) -> Self {
547        self.max_uses = UpdateField::Set(max_uses);
548        self
549    }
550
551    /// Clear the maximum number of uses (set to null)
552    pub fn clear_max_uses(mut self) -> Self {
553        self.max_uses = UpdateField::Clear;
554        self
555    }
556
557    /// Set the protected flag
558    pub fn with_protected(mut self, protected: bool) -> Self {
559        self.protected = Some(protected);
560        self
561    }
562
563    /// Set the suspended flag
564    pub fn with_suspended(mut self, suspended: bool) -> Self {
565        self.suspended = Some(suspended);
566        self
567    }
568
569    /// Set the permissions array
570    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
571        self.permissions = Some(permissions);
572        self
573    }
574
575    /// Set the metadata
576    pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
577        self.metadata = Some(metadata);
578        self
579    }
580
581    /// Convert this request to complete JSON body for the API
582    pub fn to_json_body(self) -> Value {
583        let mut attributes = serde_json::Map::new();
584
585        if let Some(name) = self.name {
586            attributes.insert("name".to_string(), json!(name));
587        }
588        if let Some(expiry) = self.expiry {
589            attributes.insert("expiry".to_string(), json!(expiry));
590        }
591        self.max_machines.apply_to(&mut attributes, "maxMachines");
592        self.max_processes.apply_to(&mut attributes, "maxProcesses");
593        self.max_users.apply_to(&mut attributes, "maxUsers");
594        self.max_cores.apply_to(&mut attributes, "maxCores");
595        self.max_uses.apply_to(&mut attributes, "maxUses");
596        if let Some(protected) = self.protected {
597            attributes.insert("protected".to_string(), json!(protected));
598        }
599        if let Some(suspended) = self.suspended {
600            attributes.insert("suspended".to_string(), json!(suspended));
601        }
602        if let Some(permissions) = self.permissions {
603            attributes.insert("permissions".to_string(), json!(permissions));
604        }
605        if let Some(metadata) = self.metadata {
606            attributes.insert("metadata".to_string(), json!(metadata));
607        }
608
609        json!({
610            "data": {
611                "type": "licenses",
612                "attributes": attributes
613            }
614        })
615    }
616}
617
618impl License {
619    pub(crate) fn from(data: KeygenResponseData<LicenseAttributes>) -> License {
620        License {
621            id: data.id,
622            scheme: None,
623            key: data.attributes.key,
624            name: data.attributes.name,
625            expiry: data.attributes.expiry,
626            status: data.attributes.status,
627            uses: data.attributes.uses,
628            max_machines: data.attributes.max_machines,
629            max_cores: data.attributes.max_cores,
630            max_uses: data.attributes.max_uses,
631            max_processes: data.attributes.max_processes,
632            max_users: data.attributes.max_users,
633            protected: data.attributes.protected,
634            suspended: data.attributes.suspended,
635            permissions: data.attributes.permissions,
636            policy: data.relationships.policy_id(),
637            metadata: data.attributes.metadata,
638            account_id: data.relationships.account_id(),
639            product_id: data.relationships.product_id(),
640            group_id: data.relationships.group_id(),
641            owner_id: data.relationships.owner_id(),
642            config: None,
643        }
644    }
645
646    pub(crate) fn from_signed_key(scheme: SchemeCode, signed_key: &str) -> License {
647        License {
648            id: String::new(),
649            scheme: Some(scheme),
650            key: signed_key.to_string(),
651            name: None,
652            expiry: None,
653            status: None,
654            uses: None,
655            max_machines: None,
656            max_cores: None,
657            max_uses: None,
658            max_processes: None,
659            max_users: None,
660            protected: None,
661            suspended: None,
662            permissions: None,
663            policy: None,
664            metadata: HashMap::new(),
665            account_id: None,
666            product_id: None,
667            group_id: None,
668            owner_id: None,
669            config: None,
670        }
671    }
672
673    /// Creates a new License with just the key
674    pub fn from_key(key: &str) -> Self {
675        License {
676            id: String::new(),
677            scheme: None,
678            key: key.to_string(),
679            name: None,
680            expiry: None,
681            status: None,
682            uses: None,
683            max_machines: None,
684            max_cores: None,
685            max_uses: None,
686            max_processes: None,
687            max_users: None,
688            protected: None,
689            suspended: None,
690            permissions: None,
691            policy: None,
692            metadata: HashMap::new(),
693            account_id: None,
694            product_id: None,
695            group_id: None,
696            owner_id: None,
697            config: None,
698        }
699    }
700
701    /// Associates a configuration with this License
702    pub fn with_config(mut self, config: KeygenConfig) -> Self {
703        self.config = Some(Arc::new(config));
704        self
705    }
706
707    /// Gets a client for this license, using the associated config or global config
708    fn get_client(&self) -> Result<Client, Error> {
709        let config = if let Some(ref cfg) = self.config {
710            cfg.as_ref().clone()
711        } else {
712            get_config()?
713        };
714        Client::new(ClientOptions::from(config))
715    }
716
717    fn build_scope(
718        config: &KeygenConfig,
719        fingerprints: &[String],
720        entitlements: &[String],
721    ) -> Result<Value, Error> {
722        let mut scope = json!({
723            "product": config.product.to_string(),
724        });
725
726        if !fingerprints.is_empty() {
727            scope["fingerprint"] = json!(fingerprints[0]);
728            if fingerprints.len() > 1 {
729                scope["components"] = json!(fingerprints[1..].to_vec());
730            }
731        }
732
733        if !entitlements.is_empty() {
734            scope["entitlements"] = json!(entitlements);
735        }
736
737        if let Some(env) = config.environment.as_ref() {
738            scope["environment"] = json!(env);
739        }
740
741        Ok(scope)
742    }
743
744    pub async fn validate(
745        self,
746        fingerprints: &[String],
747        entitlements: &[String],
748    ) -> Result<License, Error> {
749        let client = self.get_client()?;
750        let config = if let Some(ref cfg) = self.config {
751            cfg.as_ref()
752        } else {
753            &get_config()?
754        };
755        let scope = Self::build_scope(config, fingerprints, entitlements)?;
756        let params = json!({
757            "meta": {
758                "nonce": chrono::Utc::now().timestamp(),
759                "scope": scope
760            }
761        });
762
763        let response = client
764            .post(
765                &format!("licenses/{}/actions/validate", self.id),
766                Some(&params),
767                None::<&()>,
768            )
769            .await?;
770        let validation: LicenseResponse<ValidationMeta> = serde_json::from_value(response.body)?;
771        let meta = validation.meta.clone().unwrap();
772        if !meta.valid {
773            return Err(self.handle_validation_code(&meta));
774        };
775        let license = License::from(validation.data);
776        Ok(if let Some(cfg) = self.config {
777            license.with_config((*cfg).clone())
778        } else {
779            license
780        })
781    }
782
783    pub async fn validate_key(
784        self,
785        fingerprints: &[String],
786        entitlements: &[String],
787    ) -> Result<License, Error> {
788        let client = self.get_client()?;
789        let config = if let Some(ref cfg) = self.config {
790            cfg.as_ref()
791        } else {
792            &get_config()?
793        };
794        let scope = Self::build_scope(config, fingerprints, entitlements)?;
795        let params = json!({
796            "meta": {
797                "key": self.key.clone(),
798                "scope": scope
799            }
800        });
801
802        let response = client
803            .post("licenses/actions/validate-key", Some(&params), None::<&()>)
804            .await?;
805        let validation: LicenseResponse<ValidationMeta> = serde_json::from_value(response.body)?;
806        let meta = validation.meta.clone().unwrap();
807        if !meta.valid {
808            return Err(self.handle_validation_code(&meta));
809        };
810        let license = License::from(validation.data);
811        Ok(if let Some(cfg) = self.config {
812            license.with_config((*cfg).clone())
813        } else {
814            license
815        })
816    }
817
818    #[must_use = "verification result should be checked"]
819    pub fn verify(&self) -> Result<Vec<u8>, Error> {
820        if self.scheme.is_none() {
821            return Err(Error::LicenseNotSigned);
822        }
823        let config = if let Some(ref cfg) = self.config {
824            cfg.as_ref().clone()
825        } else {
826            get_config()?
827        };
828        if let Some(public_key) = &config.public_key {
829            let verifier = Verifier::new(public_key.clone());
830            verifier.verify_license(self)
831        } else {
832            Err(Error::PublicKeyMissing)
833        }
834    }
835
836    pub async fn activate(
837        &self,
838        fingerprint: &str,
839        components: &[Component],
840    ) -> Result<Machine, Error> {
841        #[cfg(not(target_arch = "wasm32"))]
842        let hostname = hostname::get()
843            .map(|h| h.to_string_lossy().into_owned())
844            .unwrap_or_else(|_| String::from("unknown"));
845        #[cfg(target_arch = "wasm32")]
846        let hostname = String::from("wasm");
847
848        let config = if let Some(ref cfg) = self.config {
849            cfg.as_ref()
850        } else {
851            &get_config()?
852        };
853        let platform = config
854            .platform
855            .clone()
856            .or_else(|| Some(format!("{}/{}", env::consts::OS, env::consts::ARCH)));
857
858        #[cfg(not(target_arch = "wasm32"))]
859        let cores = num_cpus::get();
860        #[cfg(target_arch = "wasm32")]
861        let cores = 0usize;
862
863        let mut params = json!({
864          "data": {
865            "type": "machines",
866            "attributes": {
867              "fingerprint": fingerprint,
868              "cores": cores,
869              "hostname": hostname,
870              "platform": platform,
871            },
872            "relationships": {
873              "license": {
874                "data": {
875                  "type": "licenses",
876                  "id": self.id
877                }
878              },
879            }
880          }
881        });
882        if !components.is_empty() {
883            params["data"]["relationships"]["components"] = json!({
884                "data": components
885                    .iter()
886                    .map(|comp| json!({
887                        "type": "components",
888                        "attributes": {
889                            "fingerprint": comp.fingerprint,
890                            "name": comp.name
891                        }
892                    }))
893                    .collect::<Vec<serde_json::Value>>()
894            });
895        }
896
897        let client = self.get_client()?;
898        let response = client.post("machines", Some(&params), None::<&()>).await?;
899        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
900        let machine = Machine::from(machine_response.data);
901        Ok(machine)
902    }
903
904    pub async fn deactivate(&self, id: &str) -> Result<(), Error> {
905        let client = self.get_client()?;
906        let _response = client
907            .delete::<(), serde_json::Value>(&format!("machines/{id}"), None::<&()>)
908            .await?;
909        Ok(())
910    }
911
912    pub async fn machine(&self, id: &str) -> Result<Machine, Error> {
913        let client = self.get_client()?;
914        let response = client.get(&format!("machines/{id}"), None::<&()>).await?;
915        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
916        let machine = Machine::from(machine_response.data).with_config(
917            self.config
918                .as_ref()
919                .ok_or(Error::MissingConfiguration)?
920                .as_ref()
921                .clone(),
922        );
923        Ok(machine)
924    }
925
926    pub async fn machines(
927        &self,
928        options: Option<&PaginationOptions>,
929    ) -> Result<Vec<Machine>, Error> {
930        let mut query = json!({});
931
932        if let Some(opts) = options {
933            if let Some(limit) = opts.limit {
934                query["limit"] = json!(limit);
935            } else {
936                query["limit"] = json!(100);
937            }
938
939            if let Some(page_number) = opts.page_number {
940                query["page[number]"] = json!(page_number);
941            }
942
943            if let Some(page_size) = opts.page_size {
944                query["page[size]"] = json!(page_size);
945            }
946        } else {
947            query["limit"] = json!(100);
948        }
949
950        let client = self.get_client()?;
951        let response = client
952            .get(&format!("licenses/{}/machines", self.id), Some(&query))
953            .await?;
954        let machines_response: MachinesResponse = serde_json::from_value(response.body)?;
955        let config = self
956            .config
957            .as_ref()
958            .ok_or(Error::MissingConfiguration)?
959            .as_ref()
960            .clone();
961        let machines = machines_response
962            .data
963            .iter()
964            .map(|d| Machine::from(d.clone()).with_config(config.clone()))
965            .collect();
966        Ok(machines)
967    }
968
969    pub async fn entitlements(
970        &self,
971        options: Option<&PaginationOptions>,
972    ) -> Result<Vec<Entitlement>, Error> {
973        let mut query = json!({});
974
975        if let Some(opts) = options {
976            if let Some(limit) = opts.limit {
977                query["limit"] = json!(limit);
978            } else {
979                query["limit"] = json!(100);
980            }
981
982            if let Some(page_number) = opts.page_number {
983                query["page[number]"] = json!(page_number);
984            }
985
986            if let Some(page_size) = opts.page_size {
987                query["page[size]"] = json!(page_size);
988            }
989        } else {
990            query["limit"] = json!(100);
991        }
992
993        let client = self.get_client()?;
994        let response = client
995            .get(&format!("licenses/{}/entitlements", self.id), Some(&query))
996            .await?;
997        let entitlements_response: EntitlementsResponse = serde_json::from_value(response.body)?;
998        let entitlements = entitlements_response
999            .data
1000            .iter()
1001            .map(|d| Entitlement::from(d.clone()))
1002            .collect();
1003        Ok(entitlements)
1004    }
1005
1006    pub async fn checkout(&self, options: &LicenseCheckoutOpts) -> Result<LicenseFile, Error> {
1007        let mut query = json!({
1008            "encrypt": 1,
1009        });
1010
1011        if let Some(ttl) = options.ttl {
1012            query["ttl"] = ttl.into();
1013        }
1014
1015        if let Some(ref include) = options.include {
1016            query["include"] = json!(include.join(","));
1017        } else {
1018            query["include"] = "entitlements".into();
1019        }
1020
1021        let client = self.get_client()?;
1022        let response = client
1023            .post(
1024                &format!("licenses/{}/actions/check-out", self.id),
1025                None::<&()>,
1026                Some(&query),
1027            )
1028            .await?;
1029        let license_file_response: CertificateFileResponse = serde_json::from_value(response.body)?;
1030        let license_file = LicenseFile::from(license_file_response.data);
1031        Ok(license_file)
1032    }
1033
1034    /// Check a license back in, invalidating any offline license file.
1035    pub async fn check_in(&self) -> Result<License, Error> {
1036        let client = self.get_client()?;
1037        let endpoint = format!("licenses/{}/actions/check-in", self.id);
1038        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1039        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1040        Ok(License::from(license_response.data))
1041    }
1042
1043    fn handle_validation_code(&self, meta: &ValidationMeta) -> Error {
1044        let code = meta.code.clone();
1045        let detail = meta.detail.clone();
1046        match code.as_str() {
1047            "FINGERPRINT_SCOPE_MISMATCH" | "NO_MACHINES" | "NO_MACHINE" => {
1048                Error::LicenseNotActivated {
1049                    code,
1050                    detail,
1051                    license: Box::new(self.clone()),
1052                }
1053            }
1054            "EXPIRED" => Error::LicenseExpired { code, detail },
1055            "SUSPENDED" => Error::LicenseSuspended { code, detail },
1056            "TOO_MANY_MACHINES" => Error::LicenseTooManyMachines { code, detail },
1057            "TOO_MANY_CORES" => Error::LicenseTooManyCores { code, detail },
1058            "TOO_MANY_PROCESSES" => Error::LicenseTooManyProcesses { code, detail },
1059            "FINGERPRINT_SCOPE_REQUIRED" | "FINGERPRINT_SCOPE_EMPTY" => {
1060                Error::ValidationFingerprintMissing { code, detail }
1061            }
1062            "COMPONENTS_SCOPE_REQUIRED" | "COMPONENTS_SCOPE_EMPTY" => {
1063                Error::ValidationComponentsMissing { code, detail }
1064            }
1065            "COMPONENTS_SCOPE_MISMATCH" => Error::ComponentNotActivated { code, detail },
1066            "HEARTBEAT_NOT_STARTED" => Error::HeartbeatRequired { code, detail },
1067            "HEARTBEAT_DEAD" => Error::HeartbeatDead { code, detail },
1068            "PRODUCT_SCOPE_REQUIRED" | "PRODUCT_SCOPE_EMPTY" => {
1069                Error::ValidationProductMissing { code, detail }
1070            }
1071            _ => Error::LicenseKeyInvalid { code, detail },
1072        }
1073    }
1074
1075    /// Create a new license using the comprehensive request structure
1076    #[cfg(feature = "token")]
1077    pub async fn create(request: LicenseCreateRequest) -> Result<License, Error> {
1078        let config = get_config()?;
1079        let client = Client::new(ClientOptions::from(config))?;
1080        let body = request.to_json_body();
1081        let response = client.post("licenses", Some(&body), None::<&()>).await?;
1082        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1083        Ok(License::from(license_response.data))
1084    }
1085
1086    /// List all licenses with optional filtering
1087    #[cfg(feature = "token")]
1088    pub async fn list(options: Option<&LicenseListOptions>) -> Result<Vec<License>, Error> {
1089        let config = get_config()?;
1090        let client = Client::new(ClientOptions::from(config))?;
1091        let mut query = json!({});
1092
1093        if let Some(opts) = options {
1094            // Pagination - following Keygen API standards
1095            if let Some(limit) = opts.limit {
1096                query["limit"] = json!(limit);
1097            }
1098            if let Some(page_number) = opts.page_number {
1099                query["page[number]"] = json!(page_number);
1100            }
1101            if let Some(page_size) = opts.page_size {
1102                query["page[size]"] = json!(page_size);
1103            }
1104
1105            // Simple filters
1106            if let Some(ref status) = opts.status {
1107                query["status"] = json!(status);
1108            }
1109            if let Some(ref product) = opts.product {
1110                query["product"] = json!(product);
1111            }
1112            if let Some(ref policy) = opts.policy {
1113                query["policy"] = json!(policy);
1114            }
1115            if let Some(ref owner) = opts.owner {
1116                query["owner"] = json!(owner);
1117            }
1118            if let Some(ref user) = opts.user {
1119                query["user"] = json!(user);
1120            }
1121            if let Some(ref group) = opts.group {
1122                query["group"] = json!(group);
1123            }
1124            if let Some(ref machine) = opts.machine {
1125                query["machine"] = json!(machine);
1126            }
1127            if let Some(assigned) = opts.assigned {
1128                query["assigned"] = json!(assigned);
1129            }
1130            if let Some(unassigned) = opts.unassigned {
1131                query["unassigned"] = json!(unassigned);
1132            }
1133            if let Some(activated) = opts.activated {
1134                query["activated"] = json!(activated);
1135            }
1136            if let Some(ref metadata) = opts.metadata {
1137                for (key, value) in metadata {
1138                    if let Some(obj) = query.as_object_mut() {
1139                        obj.insert(format!("metadata[{key}]"), value.clone());
1140                    }
1141                }
1142            }
1143            if let Some(ref activations) = opts.activations {
1144                if let Some(eq) = activations.eq {
1145                    query["activations[eq]"] = json!(eq);
1146                }
1147                if let Some(gt) = activations.gt {
1148                    query["activations[gt]"] = json!(gt);
1149                }
1150                if let Some(gte) = activations.gte {
1151                    query["activations[gte]"] = json!(gte);
1152                }
1153                if let Some(lt) = activations.lt {
1154                    query["activations[lt]"] = json!(lt);
1155                }
1156                if let Some(lte) = activations.lte {
1157                    query["activations[lte]"] = json!(lte);
1158                }
1159            }
1160            if let Some(ref expires) = opts.expires {
1161                if let Some(value) = &expires.r#in {
1162                    query["expires[in]"] = json!(value);
1163                }
1164                if let Some(value) = &expires.on {
1165                    query["expires[on]"] = json!(value);
1166                }
1167                if let Some(value) = &expires.before {
1168                    query["expires[before]"] = json!(value);
1169                }
1170                if let Some(value) = &expires.after {
1171                    query["expires[after]"] = json!(value);
1172                }
1173            }
1174            if let Some(ref expired) = opts.expired {
1175                if let Some(value) = &expired.r#in {
1176                    query["expired[in]"] = json!(value);
1177                }
1178                if let Some(value) = &expired.on {
1179                    query["expired[on]"] = json!(value);
1180                }
1181                if let Some(value) = &expired.before {
1182                    query["expired[before]"] = json!(value);
1183                }
1184                if let Some(value) = &expired.after {
1185                    query["expired[after]"] = json!(value);
1186                }
1187            }
1188            if let Some(ref activity) = opts.activity {
1189                if let Some(value) = &activity.inside {
1190                    query["activity[inside]"] = json!(value);
1191                }
1192                if let Some(value) = &activity.outside {
1193                    query["activity[outside]"] = json!(value);
1194                }
1195                if let Some(value) = &activity.before {
1196                    query["activity[before]"] = json!(value);
1197                }
1198                if let Some(value) = &activity.after {
1199                    query["activity[after]"] = json!(value);
1200                }
1201            }
1202        }
1203
1204        let response = client.get("licenses", Some(&query)).await?;
1205
1206        #[derive(Debug, Clone, Serialize, Deserialize)]
1207        struct LicensesResponse {
1208            pub data: Vec<KeygenResponseData<LicenseAttributes>>,
1209        }
1210
1211        let licenses_response: LicensesResponse = serde_json::from_value(response.body)?;
1212        Ok(licenses_response
1213            .data
1214            .into_iter()
1215            .map(License::from)
1216            .collect())
1217    }
1218
1219    /// Get a license by ID
1220    #[cfg(feature = "token")]
1221    pub async fn get(id: &str) -> Result<License, Error> {
1222        let config = get_config()?;
1223        let client = Client::new(ClientOptions::from(config))?;
1224        let endpoint = format!("licenses/{id}");
1225        let response = client.get(&endpoint, None::<&()>).await?;
1226        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1227        Ok(License::from(license_response.data))
1228    }
1229
1230    /// Update a license
1231    #[cfg(feature = "token")]
1232    pub async fn update(&self, request: LicenseUpdateRequest) -> Result<License, Error> {
1233        let client = self.get_client()?;
1234        let endpoint = format!("licenses/{}", self.id);
1235        let body = request.to_json_body();
1236        let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
1237        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1238        Ok(License::from(license_response.data))
1239    }
1240
1241    /// Delete a license
1242    #[cfg(feature = "token")]
1243    pub async fn delete(&self) -> Result<(), Error> {
1244        let client = self.get_client()?;
1245        let endpoint = format!("licenses/{}", self.id);
1246        client.delete::<(), ()>(&endpoint, None::<&()>).await?;
1247        Ok(())
1248    }
1249
1250    /// Suspend a license
1251    #[cfg(feature = "token")]
1252    pub async fn suspend(&self) -> Result<License, Error> {
1253        let client = self.get_client()?;
1254        let endpoint = format!("licenses/{}/actions/suspend", self.id);
1255        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1256        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1257        Ok(License::from(license_response.data))
1258    }
1259
1260    /// Reinstate a suspended license
1261    #[cfg(feature = "token")]
1262    pub async fn reinstate(&self) -> Result<License, Error> {
1263        let client = self.get_client()?;
1264        let endpoint = format!("licenses/{}/actions/reinstate", self.id);
1265        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1266        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1267        Ok(License::from(license_response.data))
1268    }
1269
1270    /// Renew a license
1271    #[cfg(feature = "token")]
1272    pub async fn renew(&self) -> Result<License, Error> {
1273        let client = self.get_client()?;
1274        let endpoint = format!("licenses/{}/actions/renew", self.id);
1275        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1276        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1277        Ok(License::from(license_response.data))
1278    }
1279
1280    /// Revoke a license
1281    #[cfg(feature = "token")]
1282    pub async fn revoke(&self) -> Result<(), Error> {
1283        let client = self.get_client()?;
1284        let endpoint = format!("licenses/{}/actions/revoke", self.id);
1285        client.delete::<(), ()>(&endpoint, None::<&()>).await?;
1286        Ok(())
1287    }
1288
1289    /// Increment the license's usage count by 1
1290    ///
1291    /// This operation is available to end users with license key authentication.
1292    /// It's the primary way for applications to track feature usage.
1293    pub async fn increment_usage(&self) -> Result<License, Error> {
1294        let client = self.get_client()?;
1295        let endpoint = format!("licenses/{}/actions/increment-usage", self.id);
1296        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1297        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1298        Ok(License::from(license_response.data))
1299    }
1300
1301    /// Decrement the license's usage count by 1 (Admin only)
1302    ///
1303    /// This is an administrative operation typically used to correct
1304    /// incorrect usage tracking or handle refunds.
1305    #[cfg(feature = "token")]
1306    pub async fn decrement_usage(&self) -> Result<License, Error> {
1307        let client = self.get_client()?;
1308        let endpoint = format!("licenses/{}/actions/decrement-usage", self.id);
1309        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1310        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1311        Ok(License::from(license_response.data))
1312    }
1313
1314    /// Reset the license's usage count to zero (Admin only)
1315    ///
1316    /// This is an administrative operation typically used at the start
1317    /// of a new billing period or for license resets.
1318    #[cfg(feature = "token")]
1319    pub async fn reset_usage(&self) -> Result<License, Error> {
1320        let client = self.get_client()?;
1321        let endpoint = format!("licenses/{}/actions/reset-usage", self.id);
1322        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
1323        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1324        Ok(License::from(license_response.data))
1325    }
1326
1327    /// Attach entitlements to a license
1328    #[cfg(feature = "token")]
1329    pub async fn attach_entitlements(&self, entitlement_ids: &[String]) -> Result<(), Error> {
1330        let client = Client::from_global_config()?;
1331        let endpoint = format!("licenses/{}/entitlements", self.id);
1332
1333        let data: Vec<Value> = entitlement_ids
1334            .iter()
1335            .map(|id| {
1336                json!({
1337                    "type": "entitlements",
1338                    "id": id
1339                })
1340            })
1341            .collect();
1342
1343        let body = json!({
1344            "data": data
1345        });
1346
1347        client
1348            .post::<Value, Value, ()>(&endpoint, Some(&body), None::<&()>)
1349            .await?;
1350        Ok(())
1351    }
1352
1353    /// Detach entitlements from a license
1354    #[cfg(feature = "token")]
1355    pub async fn detach_entitlements(&self, entitlement_ids: &[String]) -> Result<(), Error> {
1356        let client = Client::from_global_config()?;
1357        let endpoint = format!("licenses/{}/entitlements", self.id);
1358
1359        let data: Vec<Value> = entitlement_ids
1360            .iter()
1361            .map(|id| {
1362                json!({
1363                    "type": "entitlements",
1364                    "id": id
1365                })
1366            })
1367            .collect();
1368
1369        let body = json!({
1370            "data": data
1371        });
1372
1373        client
1374            .delete::<Value, Value>(&endpoint, Some(&body))
1375            .await?;
1376        Ok(())
1377    }
1378
1379    /// Generate a token scoped to this license.
1380    #[cfg(feature = "token")]
1381    pub async fn generate_token(
1382        &self,
1383        request: Option<CreateTokenRequest>,
1384    ) -> Result<Token, Error> {
1385        let client = self.get_client()?;
1386        let endpoint = format!("licenses/{}/tokens", self.id);
1387        let attributes = token_request_attributes(request.as_ref())?;
1388        let body = json!({
1389            "data": {
1390                "type": "tokens",
1391                "attributes": attributes
1392            }
1393        });
1394        let response = client.post(&endpoint, Some(&body), None::<&()>).await?;
1395        let token_response: TokenResponse = serde_json::from_value(response.body)?;
1396        Ok(Token::from(token_response.data))
1397    }
1398
1399    /// Attach users to this license.
1400    #[cfg(feature = "token")]
1401    pub async fn attach_users(&self, user_ids: &[String]) -> Result<(), Error> {
1402        let client = self.get_client()?;
1403        let endpoint = format!("licenses/{}/users", self.id);
1404        let data: Vec<Value> = user_ids
1405            .iter()
1406            .map(|id| {
1407                json!({
1408                    "type": "users",
1409                    "id": id
1410                })
1411            })
1412            .collect();
1413        let body = json!({ "data": data });
1414        client
1415            .post::<Value, Value, ()>(&endpoint, Some(&body), None::<&()>)
1416            .await?;
1417        Ok(())
1418    }
1419
1420    /// Detach users from this license.
1421    #[cfg(feature = "token")]
1422    pub async fn detach_users(&self, user_ids: &[String]) -> Result<(), Error> {
1423        let client = self.get_client()?;
1424        let endpoint = format!("licenses/{}/users", self.id);
1425        let data: Vec<Value> = user_ids
1426            .iter()
1427            .map(|id| {
1428                json!({
1429                    "type": "users",
1430                    "id": id
1431                })
1432            })
1433            .collect();
1434        let body = json!({ "data": data });
1435        client
1436            .delete::<Value, Value>(&endpoint, Some(&body))
1437            .await?;
1438        Ok(())
1439    }
1440
1441    /// List users attached to this license.
1442    #[cfg(feature = "token")]
1443    pub async fn users(&self, options: Option<&PaginationOptions>) -> Result<Vec<User>, Error> {
1444        let client = self.get_client()?;
1445        let endpoint = format!("licenses/{}/users", self.id);
1446        let response = client.get(&endpoint, options).await?;
1447        let users_response: LicenseUsersResponse = serde_json::from_value(response.body)?;
1448        Ok(users_response.data.into_iter().map(User::from).collect())
1449    }
1450
1451    /// Change the policy associated with this license.
1452    #[cfg(feature = "token")]
1453    pub async fn change_policy(&self, policy_id: &str) -> Result<License, Error> {
1454        let client = self.get_client()?;
1455        let endpoint = format!("licenses/{}/policy", self.id);
1456        let body = json!({
1457            "data": {
1458                "type": "policies",
1459                "id": policy_id
1460            }
1461        });
1462        let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
1463        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1464        Ok(License::from(license_response.data))
1465    }
1466
1467    /// Change the owner associated with this license.
1468    #[cfg(feature = "token")]
1469    pub async fn change_owner(&self, owner_id: &str) -> Result<License, Error> {
1470        let client = self.get_client()?;
1471        let endpoint = format!("licenses/{}/owner", self.id);
1472        let body = json!({
1473            "data": {
1474                "type": "users",
1475                "id": owner_id
1476            }
1477        });
1478        let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
1479        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1480        Ok(License::from(license_response.data))
1481    }
1482
1483    /// Change the group associated with this license.
1484    #[cfg(feature = "token")]
1485    pub async fn change_group(&self, group_id: &str) -> Result<License, Error> {
1486        let client = self.get_client()?;
1487        let endpoint = format!("licenses/{}/group", self.id);
1488        let body = json!({
1489            "data": {
1490                "type": "groups",
1491                "id": group_id
1492            }
1493        });
1494        let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
1495        let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
1496        Ok(License::from(license_response.data))
1497    }
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502    use super::*;
1503    use crate::config::{reset_config, set_config, KeygenConfig};
1504    #[cfg(feature = "token")]
1505    use chrono::TimeZone;
1506    use mockito::{mock, server_url};
1507    use serde_json::json;
1508
1509    fn create_test_license() -> License {
1510        License {
1511            id: "test_license_id".to_string(),
1512            scheme: None,
1513            name: Some("Test License".to_string()),
1514            key: "TEST-LICENSE-KEY".to_string(),
1515            expiry: None,
1516            status: None,
1517            uses: None,
1518            max_machines: None,
1519            max_cores: None,
1520            max_uses: None,
1521            max_processes: None,
1522            max_users: None,
1523            protected: None,
1524            suspended: None,
1525            permissions: None,
1526            policy: None,
1527            metadata: HashMap::new(),
1528            account_id: None,
1529            product_id: None,
1530            group_id: None,
1531            owner_id: None,
1532            config: None,
1533        }
1534    }
1535
1536    fn get_mock_body() -> String {
1537        json!({
1538            "meta": {
1539                "ts": "2021-01-01T00:00:00Z",
1540                "valid": true,
1541                "detail": "is valid",
1542                "code": "VALID",
1543                "scope": {
1544                    "fingerprint": "test_fingerprint",
1545                    "components": ["comp1", "comp2"],
1546                    "product": "test_product"
1547                }
1548            },
1549            "data": {
1550                "id": "test_license_id",
1551                "type": "licenses",
1552                "attributes": {
1553                    "name": "Test License",
1554                    "key": "TEST-LICENSE-KEY",
1555                    "expiry": null,
1556                    "status": "valid",
1557                    "metadata": {
1558                        "customer_name": "Test Customer",
1559                        "customer_email": "test@example.com",
1560                        "is_premium": true
1561                    }
1562                },
1563                "relationships": {
1564                    "policy": {
1565                        "data": {
1566                            "type": "policies",
1567                            "id": "11314277-0f31-4a77-9366-0299e9f52123"
1568                        }
1569                    }
1570                }
1571            }
1572        })
1573        .to_string()
1574    }
1575
1576    #[tokio::test]
1577    async fn test_validate() {
1578        let license = create_test_license();
1579        let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
1580            .with_status(200)
1581            .with_header("content-type", "application/json")
1582            .with_body(get_mock_body())
1583            .create();
1584
1585        set_config(KeygenConfig {
1586            api_url: server_url(),
1587            account: "test_account".to_string(),
1588            product: "test_product".to_string(),
1589            ..Default::default()
1590        })
1591        .unwrap();
1592
1593        let result = license
1594            .validate(
1595                &[
1596                    "test_fingerprint".to_string(),
1597                    "comp1".to_string(),
1598                    "comp2".to_string(),
1599                ],
1600                &[],
1601            )
1602            .await;
1603        assert!(result.is_ok());
1604        let _ = reset_config();
1605    }
1606
1607    #[tokio::test]
1608    async fn test_validate_key() {
1609        let license = create_test_license();
1610        let _m = mock("POST", "/v1/licenses/actions/validate-key")
1611            .with_status(200)
1612            .with_header("content-type", "application/json")
1613            .with_body(get_mock_body())
1614            .create();
1615
1616        let _ = set_config(KeygenConfig {
1617            api_url: server_url(),
1618            account: "test_account".to_string(),
1619            product: "test_product".to_string(),
1620            license_key: Some("TEST-LICENSE-KEY".to_string()),
1621            ..Default::default()
1622        });
1623
1624        let result = license
1625            .validate_key(
1626                &[
1627                    "test_fingerprint".to_string(),
1628                    "comp1".to_string(),
1629                    "comp2".to_string(),
1630                ],
1631                &[],
1632            )
1633            .await;
1634        assert!(result.is_ok());
1635        let _ = reset_config();
1636    }
1637
1638    #[test]
1639    fn test_verify() {
1640        let mut license = create_test_license();
1641
1642        license.scheme = Some(SchemeCode::Ed25519Sign);
1643        let result = license.verify();
1644        assert!(matches!(result, Err(Error::PublicKeyMissing)));
1645
1646        license.scheme = None;
1647        let result = license.verify();
1648        assert!(matches!(result, Err(Error::LicenseNotSigned)));
1649    }
1650
1651    #[tokio::test]
1652    async fn test_validate_with_metadata() {
1653        let license = create_test_license();
1654        let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
1655            .with_status(200)
1656            .with_header("content-type", "application/json")
1657            .with_body(get_mock_body())
1658            .create();
1659
1660        set_config(KeygenConfig {
1661            api_url: server_url(),
1662            account: "test_account".to_string(),
1663            product: "test_product".to_string(),
1664            ..Default::default()
1665        })
1666        .unwrap();
1667
1668        let result = license
1669            .validate(
1670                &[
1671                    "test_fingerprint".to_string(),
1672                    "comp1".to_string(),
1673                    "comp2".to_string(),
1674                ],
1675                &[],
1676            )
1677            .await;
1678
1679        assert!(result.is_ok());
1680        let validated_license = result.unwrap();
1681
1682        // Verify metadata fields
1683        assert!(validated_license.metadata.contains_key("customer_name"));
1684        assert_eq!(
1685            validated_license
1686                .metadata
1687                .get("customer_name")
1688                .unwrap()
1689                .as_str()
1690                .unwrap(),
1691            "Test Customer"
1692        );
1693
1694        assert!(validated_license.metadata.contains_key("customer_email"));
1695        assert_eq!(
1696            validated_license
1697                .metadata
1698                .get("customer_email")
1699                .unwrap()
1700                .as_str()
1701                .unwrap(),
1702            "test@example.com"
1703        );
1704
1705        assert!(validated_license.metadata.contains_key("is_premium"));
1706        assert!(validated_license
1707            .metadata
1708            .get("is_premium")
1709            .unwrap()
1710            .as_bool()
1711            .unwrap());
1712
1713        let _ = reset_config();
1714    }
1715
1716    #[tokio::test]
1717    async fn test_pagination_options() {
1718        let _m = mock("GET", "/v1/licenses/test_license_id/machines")
1719            .match_query(mockito::Matcher::AllOf(vec![
1720                mockito::Matcher::UrlEncoded("limit".into(), "50".into()),
1721                mockito::Matcher::UrlEncoded("page[number]".into(), "2".into()),
1722                mockito::Matcher::UrlEncoded("page[size]".into(), "10".into()),
1723            ]))
1724            .with_status(200)
1725            .with_header("content-type", "application/json")
1726            .with_body(
1727                json!({
1728                    "data": []
1729                })
1730                .to_string(),
1731            )
1732            .create();
1733
1734        set_config(KeygenConfig {
1735            api_url: server_url(),
1736            account: "test_account".to_string(),
1737            product: "test_product".to_string(),
1738            ..Default::default()
1739        })
1740        .unwrap();
1741
1742        let config = KeygenConfig {
1743            api_url: server_url(),
1744            account: "test_account".to_string(),
1745            product: "test_product".to_string(),
1746            ..Default::default()
1747        };
1748        let license = create_test_license().with_config(config);
1749
1750        let pagination_options = PaginationOptions {
1751            limit: Some(50),
1752            page_number: Some(2),
1753            page_size: Some(10),
1754        };
1755
1756        let result = license.machines(Some(&pagination_options)).await;
1757        match &result {
1758            Ok(_) => println!("Test passed"),
1759            Err(e) => println!("Test failed with error: {:?}", e),
1760        }
1761        assert!(result.is_ok());
1762        let _ = reset_config();
1763    }
1764
1765    #[tokio::test]
1766    async fn test_validation_errors() {
1767        let license = create_test_license();
1768
1769        // Test expired license
1770        let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
1771            .with_status(200)
1772            .with_header("content-type", "application/json")
1773            .with_body(
1774                json!({
1775                    "meta": {
1776                        "ts": "2021-01-01T00:00:00Z",
1777                        "valid": false,
1778                        "detail": "license expired",
1779                        "code": "EXPIRED",
1780                        "scope": {
1781                            "fingerprint": "test_fingerprint"
1782                        }
1783                    },
1784                    "data": {
1785                        "id": "test_license_id",
1786                        "type": "licenses",
1787                        "attributes": {
1788                            "key": "TEST-LICENSE-KEY",
1789                            "name": "Test License",
1790                            "expiry": null,
1791                            "status": "expired",
1792                            "uses": null,
1793                            "maxMachines": null,
1794                            "maxCores": null,
1795                            "maxUses": null,
1796                            "maxProcesses": null,
1797                            "protected": null,
1798                            "suspended": null,
1799                            "metadata": {}
1800                        },
1801                        "relationships": {
1802                            "policy": {
1803                                "data": {
1804                                    "type": "policies",
1805                                    "id": "policy_123"
1806                                }
1807                            }
1808                        }
1809                    }
1810                })
1811                .to_string(),
1812            )
1813            .create();
1814
1815        set_config(KeygenConfig {
1816            api_url: server_url(),
1817            account: "test_account".to_string(),
1818            product: "test_product".to_string(),
1819            ..Default::default()
1820        })
1821        .unwrap();
1822
1823        let result = license
1824            .validate(&["test_fingerprint".to_string()], &[])
1825            .await;
1826        assert!(matches!(result, Err(Error::LicenseExpired { .. })));
1827        let _ = reset_config();
1828    }
1829
1830    #[tokio::test]
1831    async fn test_validation_with_empty_scope() {
1832        let license = create_test_license();
1833        let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
1834            .with_status(200)
1835            .with_header("content-type", "application/json")
1836            .with_body(get_mock_body())
1837            .create();
1838
1839        set_config(KeygenConfig {
1840            api_url: server_url(),
1841            account: "test_account".to_string(),
1842            product: "test_product".to_string(),
1843            ..Default::default()
1844        })
1845        .unwrap();
1846
1847        let result = license.validate(&[], &[]).await;
1848        assert!(result.is_ok());
1849        let _ = reset_config();
1850    }
1851
1852    #[tokio::test]
1853    async fn test_license_with_all_attributes() {
1854        let license = create_test_license();
1855        let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
1856            .with_status(200)
1857            .with_header("content-type", "application/json")
1858            .with_body(
1859                json!({
1860                    "meta": {
1861                        "ts": "2021-01-01T00:00:00Z",
1862                        "valid": true,
1863                        "detail": "is valid",
1864                        "code": "VALID",
1865                        "scope": {
1866                            "fingerprint": "test_fingerprint"
1867                        }
1868                    },
1869                    "data": {
1870                        "id": "test_license_id",
1871                        "type": "licenses",
1872                        "attributes": {
1873                            "key": "TEST-LICENSE-KEY",
1874                            "name": "Test License",
1875                            "expiry": "2025-12-31T23:59:59Z",
1876                            "status": "active",
1877                            "uses": 5,
1878                            "maxMachines": 10,
1879                            "maxCores": 20,
1880                            "maxUses": 100,
1881                            "maxProcesses": 5,
1882                            "protected": true,
1883                            "suspended": false,
1884                            "metadata": {
1885                                "tier": "premium",
1886                                "features": ["feature_a", "feature_b"]
1887                            }
1888                        },
1889                        "relationships": {
1890                            "policy": {
1891                                "data": {
1892                                    "type": "policies",
1893                                    "id": "policy_123"
1894                                }
1895                            }
1896                        }
1897                    }
1898                })
1899                .to_string(),
1900            )
1901            .create();
1902
1903        set_config(KeygenConfig {
1904            api_url: server_url(),
1905            account: "test_account".to_string(),
1906            product: "test_product".to_string(),
1907            ..Default::default()
1908        })
1909        .unwrap();
1910
1911        let result = license
1912            .validate(&["test_fingerprint".to_string()], &[])
1913            .await;
1914        assert!(result.is_ok());
1915
1916        let validated_license = result.unwrap();
1917        assert_eq!(validated_license.uses, Some(5));
1918        assert_eq!(validated_license.max_machines, Some(10));
1919        assert_eq!(validated_license.max_cores, Some(20));
1920        assert_eq!(validated_license.max_uses, Some(100));
1921        assert_eq!(validated_license.max_processes, Some(5));
1922        assert!(validated_license.protected == Some(true));
1923        assert_eq!(validated_license.suspended, Some(false));
1924        assert!(validated_license.metadata.contains_key("tier"));
1925        assert_eq!(
1926            validated_license
1927                .metadata
1928                .get("tier")
1929                .unwrap()
1930                .as_str()
1931                .unwrap(),
1932            "premium"
1933        );
1934
1935        let _ = reset_config();
1936    }
1937
1938    #[tokio::test]
1939    async fn test_machine_activation_errors() {
1940        let license = create_test_license();
1941        let _m = mock("POST", "/v1/machines")
1942            .with_status(422)
1943            .with_header("content-type", "application/json")
1944            .with_body(
1945                json!({
1946                    "errors": [{
1947                        "title": "Unprocessable Entity",
1948                        "detail": "License has reached machine limit",
1949                        "code": "MACHINE_LIMIT_EXCEEDED"
1950                    }]
1951                })
1952                .to_string(),
1953            )
1954            .create();
1955
1956        set_config(KeygenConfig {
1957            api_url: server_url(),
1958            account: "test_account".to_string(),
1959            product: "test_product".to_string(),
1960            ..Default::default()
1961        })
1962        .unwrap();
1963
1964        let result = license.activate("test_fingerprint", &[]).await;
1965        assert!(result.is_err());
1966        let _ = reset_config();
1967    }
1968
1969    #[test]
1970    fn test_license_relationships() {
1971        use crate::{
1972            KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
1973        };
1974
1975        // Test that all relationship IDs are properly extracted
1976        let license_data = KeygenResponseData {
1977            id: "test-license-id".to_string(),
1978            r#type: "licenses".to_string(),
1979            attributes: LicenseAttributes {
1980                key: "TEST-LICENSE-KEY".to_string(),
1981                name: Some("Test License".to_string()),
1982                expiry: None,
1983                status: Some("active".to_string()),
1984                uses: Some(5),
1985                max_machines: Some(10),
1986                max_cores: Some(20),
1987                max_uses: Some(100),
1988                max_processes: Some(5),
1989                max_users: None,
1990                protected: Some(true),
1991                suspended: Some(false),
1992                permissions: None,
1993                metadata: HashMap::new(),
1994            },
1995            relationships: KeygenRelationships {
1996                policy: Some(KeygenRelationship {
1997                    data: Some(KeygenRelationshipData {
1998                        r#type: "policies".to_string(),
1999                        id: "test-policy-id".to_string(),
2000                    }),
2001                    links: None,
2002                }),
2003                account: Some(KeygenRelationship {
2004                    data: Some(KeygenRelationshipData {
2005                        r#type: "accounts".to_string(),
2006                        id: "test-account-id".to_string(),
2007                    }),
2008                    links: None,
2009                }),
2010                product: Some(KeygenRelationship {
2011                    data: Some(KeygenRelationshipData {
2012                        r#type: "products".to_string(),
2013                        id: "test-product-id".to_string(),
2014                    }),
2015                    links: None,
2016                }),
2017                group: Some(KeygenRelationship {
2018                    data: Some(KeygenRelationshipData {
2019                        r#type: "groups".to_string(),
2020                        id: "test-group-id".to_string(),
2021                    }),
2022                    links: None,
2023                }),
2024                owner: Some(KeygenRelationship {
2025                    data: Some(KeygenRelationshipData {
2026                        r#type: "users".to_string(),
2027                        id: "test-owner-id".to_string(),
2028                    }),
2029                    links: None,
2030                }),
2031                users: None,
2032                machines: None,
2033                environment: None,
2034                license: None,
2035                release: None,
2036                other: HashMap::new(),
2037            },
2038        };
2039
2040        let license = License::from(license_data);
2041
2042        assert_eq!(license.policy, Some("test-policy-id".to_string()));
2043        assert_eq!(license.account_id, Some("test-account-id".to_string()));
2044        assert_eq!(license.product_id, Some("test-product-id".to_string()));
2045        assert_eq!(license.group_id, Some("test-group-id".to_string()));
2046        assert_eq!(license.owner_id, Some("test-owner-id".to_string()));
2047        assert_eq!(license.id, "test-license-id");
2048        assert_eq!(license.key, "TEST-LICENSE-KEY");
2049    }
2050
2051    #[test]
2052    fn test_license_without_relationships() {
2053        use crate::{KeygenRelationships, KeygenResponseData};
2054
2055        // Test that all relationship IDs are None when no relationships exist
2056        let license_data = KeygenResponseData {
2057            id: "test-license-id".to_string(),
2058            r#type: "licenses".to_string(),
2059            attributes: LicenseAttributes {
2060                key: "TEST-LICENSE-KEY".to_string(),
2061                name: Some("Test License".to_string()),
2062                expiry: None,
2063                status: Some("active".to_string()),
2064                uses: None,
2065                max_machines: None,
2066                max_cores: None,
2067                max_uses: None,
2068                max_processes: None,
2069                max_users: None,
2070                protected: None,
2071                suspended: None,
2072                permissions: None,
2073                metadata: HashMap::new(),
2074            },
2075            relationships: KeygenRelationships {
2076                policy: None,
2077                account: None,
2078                product: None,
2079                group: None,
2080                owner: None,
2081                users: None,
2082                machines: None,
2083                environment: None,
2084                license: None,
2085                release: None,
2086                other: HashMap::new(),
2087            },
2088        };
2089
2090        let license = License::from(license_data);
2091
2092        assert_eq!(license.policy, None);
2093        assert_eq!(license.account_id, None);
2094        assert_eq!(license.product_id, None);
2095        assert_eq!(license.group_id, None);
2096        assert_eq!(license.owner_id, None);
2097    }
2098
2099    #[cfg(feature = "token")]
2100    #[tokio::test]
2101    async fn test_create_license_basic() {
2102        let _m = mock("POST", "/v1/licenses")
2103            .with_status(201)
2104            .with_header("content-type", "application/json")
2105            .with_body(
2106                json!({
2107                    "data": {
2108                        "id": "license-123",
2109                        "type": "licenses",
2110                        "attributes": {
2111                            "key": "LICENSE-KEY-123",
2112                            "name": "Test License",
2113                            "expiry": null,
2114                            "status": "active",
2115                            "uses": null,
2116                            "maxMachines": 5,
2117                            "maxCores": null,
2118                            "maxUses": null,
2119                            "maxProcesses": null,
2120                            "protected": null,
2121                            "suspended": false,
2122                            "metadata": {
2123                                "tier": "premium"
2124                            }
2125                        },
2126                        "relationships": {
2127                            "policy": {
2128                                "data": {
2129                                    "type": "policies",
2130                                    "id": "policy-123"
2131                                }
2132                            },
2133                            "owner": {
2134                                "data": {
2135                                    "type": "users",
2136                                    "id": "user-123"
2137                                }
2138                            }
2139                        }
2140                    }
2141                })
2142                .to_string(),
2143            )
2144            .create();
2145
2146        let _ = set_config(KeygenConfig {
2147            api_url: server_url(),
2148            account: "test_account".to_string(),
2149            token: Some("admin-token".to_string()),
2150            ..Default::default()
2151        });
2152
2153        let mut metadata = HashMap::new();
2154        metadata.insert("tier".to_string(), json!("premium"));
2155
2156        let request = LicenseCreateRequest::new("policy-123".to_string())
2157            .with_name("Test License".to_string())
2158            .with_max_machines(5)
2159            .with_owner_id("user-123".to_string())
2160            .with_metadata(metadata);
2161
2162        let result = License::create(request).await;
2163
2164        assert!(result.is_ok());
2165        let license = result.unwrap();
2166        assert_eq!(license.id, "license-123");
2167        assert_eq!(license.key, "LICENSE-KEY-123");
2168        assert_eq!(license.name, Some("Test License".to_string()));
2169        assert_eq!(license.max_machines, Some(5));
2170        assert_eq!(license.owner_id, Some("user-123".to_string()));
2171        assert!(license.metadata.contains_key("tier"));
2172
2173        let _ = reset_config();
2174    }
2175
2176    #[cfg(feature = "token")]
2177    #[tokio::test]
2178    async fn test_create_license_with_custom_key() {
2179        let _m = mock("POST", "/v1/licenses")
2180            .with_status(201)
2181            .with_header("content-type", "application/json")
2182            .with_body(
2183                json!({
2184                    "data": {
2185                        "id": "license-456",
2186                        "type": "licenses",
2187                        "attributes": {
2188                            "key": "CUSTOM-LICENSE-KEY",
2189                            "name": "Custom License",
2190                            "expiry": "2025-12-31T23:59:59Z",
2191                            "status": "active",
2192                            "uses": null,
2193                            "maxMachines": 10,
2194                            "maxCores": null,
2195                            "maxUses": null,
2196                            "maxProcesses": null,
2197                            "protected": null,
2198                            "suspended": false,
2199                            "metadata": {}
2200                        },
2201                        "relationships": {
2202                            "policy": {
2203                                "data": {
2204                                    "type": "policies",
2205                                    "id": "policy-456"
2206                                }
2207                            },
2208                            "group": {
2209                                "data": {
2210                                    "type": "groups",
2211                                    "id": "group-456"
2212                                }
2213                            }
2214                        }
2215                    }
2216                })
2217                .to_string(),
2218            )
2219            .create();
2220
2221        let _ = set_config(KeygenConfig {
2222            api_url: server_url(),
2223            account: "test_account".to_string(),
2224            token: Some("admin-token".to_string()),
2225            ..Default::default()
2226        });
2227
2228        let expiry = Utc.from_utc_datetime(
2229            &chrono::NaiveDate::from_ymd_opt(2025, 12, 31)
2230                .unwrap()
2231                .and_hms_opt(23, 59, 59)
2232                .unwrap(),
2233        );
2234
2235        let request = LicenseCreateRequest::new("policy-456".to_string())
2236            .with_name("Custom License".to_string())
2237            .with_key("CUSTOM-LICENSE-KEY".to_string())
2238            .with_expiry(expiry)
2239            .with_max_machines(10)
2240            .with_group_id("group-456".to_string());
2241
2242        let result = License::create(request).await;
2243
2244        assert!(result.is_ok());
2245        let license = result.unwrap();
2246        assert_eq!(license.id, "license-456");
2247        assert_eq!(license.key, "CUSTOM-LICENSE-KEY");
2248        assert_eq!(license.name, Some("Custom License".to_string()));
2249        assert_eq!(license.max_machines, Some(10));
2250        assert_eq!(license.group_id, Some("group-456".to_string()));
2251        assert!(license.owner_id.is_none());
2252
2253        let _ = reset_config();
2254    }
2255
2256    #[cfg(feature = "token")]
2257    #[tokio::test]
2258    async fn test_create_license_minimal() {
2259        let _m = mock("POST", "/v1/licenses")
2260            .with_status(201)
2261            .with_header("content-type", "application/json")
2262            .with_body(
2263                json!({
2264                    "data": {
2265                        "id": "license-789",
2266                        "type": "licenses",
2267                        "attributes": {
2268                            "key": "AUTO-GENERATED-KEY",
2269                            "name": null,
2270                            "expiry": null,
2271                            "status": "active",
2272                            "uses": null,
2273                            "maxMachines": null,
2274                            "maxCores": null,
2275                            "maxUses": null,
2276                            "maxProcesses": null,
2277                            "protected": null,
2278                            "suspended": false,
2279                            "metadata": {}
2280                        },
2281                        "relationships": {
2282                            "policy": {
2283                                "data": {
2284                                    "type": "policies",
2285                                    "id": "policy-789"
2286                                }
2287                            }
2288                        }
2289                    }
2290                })
2291                .to_string(),
2292            )
2293            .create();
2294
2295        let _ = set_config(KeygenConfig {
2296            api_url: server_url(),
2297            account: "test_account".to_string(),
2298            token: Some("admin-token".to_string()),
2299            ..Default::default()
2300        });
2301
2302        let request = LicenseCreateRequest::new("policy-789".to_string());
2303        let result = License::create(request).await;
2304
2305        assert!(result.is_ok());
2306        let license = result.unwrap();
2307        assert_eq!(license.id, "license-789");
2308        assert_eq!(license.key, "AUTO-GENERATED-KEY");
2309        assert!(license.name.is_none());
2310        assert!(license.max_machines.is_none());
2311        assert!(license.owner_id.is_none());
2312        assert!(license.group_id.is_none());
2313
2314        let _ = reset_config();
2315    }
2316
2317    #[cfg(feature = "token")]
2318    #[tokio::test]
2319    async fn test_create_license_error() {
2320        let _m = mock("POST", "/v1/licenses")
2321            .with_status(422)
2322            .with_header("content-type", "application/json")
2323            .with_body(
2324                json!({
2325                    "errors": [
2326                        {
2327                            "title": "Unprocessable Entity",
2328                            "detail": "Policy is required",
2329                            "code": "MISSING_POLICY"
2330                        }
2331                    ]
2332                })
2333                .to_string(),
2334            )
2335            .create();
2336
2337        let _ = set_config(KeygenConfig {
2338            api_url: server_url(),
2339            account: "test_account".to_string(),
2340            token: Some("admin-token".to_string()),
2341            ..Default::default()
2342        });
2343
2344        let request = LicenseCreateRequest::new("invalid-policy".to_string());
2345        let result = License::create(request).await;
2346
2347        assert!(result.is_err());
2348        let _ = reset_config();
2349    }
2350
2351    #[cfg(feature = "token")]
2352    #[tokio::test]
2353    async fn test_create_license_with_all_parameters() {
2354        let _m = mock("POST", "/v1/licenses")
2355            .with_status(201)
2356            .with_header("content-type", "application/json")
2357            .with_body(
2358                json!({
2359                    "data": {
2360                        "id": "license-comprehensive",
2361                        "type": "licenses",
2362                        "attributes": {
2363                            "key": "COMPREHENSIVE-LICENSE-KEY",
2364                            "name": "Comprehensive License",
2365                            "expiry": "2025-12-31T23:59:59Z",
2366                            "status": "active",
2367                            "uses": null,
2368                            "maxMachines": 10,
2369                            "maxProcesses": 5,
2370                            "maxUsers": 3,
2371                            "maxCores": 8,
2372                            "maxUses": 100,
2373                            "protected": true,
2374                            "suspended": false,
2375                            "permissions": ["activate", "deactivate", "read"],
2376                            "metadata": {
2377                                "tier": "enterprise",
2378                                "features": ["advanced", "premium"]
2379                            }
2380                        },
2381                        "relationships": {
2382                            "policy": {
2383                                "data": {
2384                                    "type": "policies",
2385                                    "id": "policy-comprehensive"
2386                                }
2387                            },
2388                            "owner": {
2389                                "data": {
2390                                    "type": "users",
2391                                    "id": "user-comprehensive"
2392                                }
2393                            },
2394                            "group": {
2395                                "data": {
2396                                    "type": "groups",
2397                                    "id": "group-comprehensive"
2398                                }
2399                            }
2400                        }
2401                    }
2402                })
2403                .to_string(),
2404            )
2405            .create();
2406
2407        let _ = set_config(KeygenConfig {
2408            api_url: server_url(),
2409            account: "test_account".to_string(),
2410            token: Some("admin-token".to_string()),
2411            ..Default::default()
2412        });
2413
2414        let expiry = Utc.from_utc_datetime(
2415            &chrono::NaiveDate::from_ymd_opt(2025, 12, 31)
2416                .unwrap()
2417                .and_hms_opt(23, 59, 59)
2418                .unwrap(),
2419        );
2420        let mut metadata = HashMap::new();
2421        metadata.insert("tier".to_string(), json!("enterprise"));
2422        metadata.insert("features".to_string(), json!(["advanced", "premium"]));
2423
2424        let request = LicenseCreateRequest::new("policy-comprehensive".to_string())
2425            .with_name("Comprehensive License".to_string())
2426            .with_key("COMPREHENSIVE-LICENSE-KEY".to_string())
2427            .with_expiry(expiry)
2428            .with_max_machines(10)
2429            .with_max_processes(5)
2430            .with_max_users(3)
2431            .with_max_cores(8)
2432            .with_max_uses(100)
2433            .with_protected(true)
2434            .with_suspended(false)
2435            .with_permissions(vec![
2436                "activate".to_string(),
2437                "deactivate".to_string(),
2438                "read".to_string(),
2439            ])
2440            .with_metadata(metadata)
2441            .with_owner_id("user-comprehensive".to_string())
2442            .with_group_id("group-comprehensive".to_string());
2443
2444        let result = License::create(request).await;
2445
2446        assert!(result.is_ok());
2447        let license = result.unwrap();
2448        assert_eq!(license.id, "license-comprehensive");
2449        assert_eq!(license.key, "COMPREHENSIVE-LICENSE-KEY");
2450        assert_eq!(license.name, Some("Comprehensive License".to_string()));
2451        assert_eq!(license.max_machines, Some(10));
2452        assert_eq!(license.max_processes, Some(5));
2453        assert_eq!(license.max_cores, Some(8));
2454        assert_eq!(license.max_uses, Some(100));
2455        assert!(license.protected == Some(true));
2456        assert_eq!(license.suspended, Some(false));
2457        assert_eq!(license.owner_id, Some("user-comprehensive".to_string()));
2458        assert_eq!(license.group_id, Some("group-comprehensive".to_string()));
2459        assert!(license.metadata.contains_key("tier"));
2460        assert_eq!(
2461            license.metadata.get("tier").unwrap().as_str().unwrap(),
2462            "enterprise"
2463        );
2464
2465        let _ = reset_config();
2466    }
2467
2468    #[cfg(feature = "token")]
2469    #[tokio::test]
2470    async fn test_update_license_comprehensive() {
2471        let license = create_test_license();
2472        let _m = mock("PATCH", "/v1/licenses/test_license_id")
2473            .with_status(200)
2474            .with_header("content-type", "application/json")
2475            .with_body(
2476                json!({
2477                    "data": {
2478                        "id": "test_license_id",
2479                        "type": "licenses",
2480                        "attributes": {
2481                            "key": "TEST-LICENSE-KEY",
2482                            "name": "Updated License Name",
2483                            "expiry": "2025-12-31T23:59:59Z",
2484                            "status": "active",
2485                            "uses": null,
2486                            "maxMachines": 20,
2487                            "maxProcesses": 10,
2488                            "maxUsers": 5,
2489                            "maxCores": 16,
2490                            "maxUses": 200,
2491                            "protected": true,
2492                            "suspended": false,
2493                            "permissions": ["read", "write", "activate"],
2494                            "metadata": {
2495                                "tier": "enterprise",
2496                                "updated": true
2497                            }
2498                        },
2499                        "relationships": {
2500                            "policy": {
2501                                "data": {
2502                                    "type": "policies",
2503                                    "id": "policy-123"
2504                                }
2505                            }
2506                        }
2507                    }
2508                })
2509                .to_string(),
2510            )
2511            .create();
2512
2513        let _ = set_config(KeygenConfig {
2514            api_url: server_url(),
2515            account: "test_account".to_string(),
2516            token: Some("admin-token".to_string()),
2517            ..Default::default()
2518        });
2519
2520        let expiry = Utc.from_utc_datetime(
2521            &chrono::NaiveDate::from_ymd_opt(2025, 12, 31)
2522                .unwrap()
2523                .and_hms_opt(23, 59, 59)
2524                .unwrap(),
2525        );
2526        let mut metadata = HashMap::new();
2527        metadata.insert("tier".to_string(), json!("enterprise"));
2528        metadata.insert("updated".to_string(), json!(true));
2529
2530        let request = LicenseUpdateRequest::new()
2531            .with_name("Updated License Name".to_string())
2532            .with_expiry(expiry)
2533            .with_max_machines(20)
2534            .with_max_processes(10)
2535            .with_max_users(5)
2536            .with_max_cores(16)
2537            .with_max_uses(200)
2538            .with_protected(true)
2539            .with_suspended(false)
2540            .with_permissions(vec![
2541                "read".to_string(),
2542                "write".to_string(),
2543                "activate".to_string(),
2544            ])
2545            .with_metadata(metadata);
2546
2547        let result = license.update(request).await;
2548
2549        assert!(result.is_ok());
2550        let updated_license = result.unwrap();
2551        assert_eq!(updated_license.id, "test_license_id");
2552        assert_eq!(
2553            updated_license.name,
2554            Some("Updated License Name".to_string())
2555        );
2556        assert_eq!(updated_license.max_machines, Some(20));
2557        assert_eq!(updated_license.max_processes, Some(10));
2558        assert_eq!(updated_license.max_users, Some(5));
2559        assert_eq!(updated_license.max_cores, Some(16));
2560        assert_eq!(updated_license.max_uses, Some(200));
2561        assert!(updated_license.protected == Some(true));
2562        assert_eq!(updated_license.suspended, Some(false));
2563        assert!(updated_license.metadata.contains_key("tier"));
2564        assert_eq!(
2565            updated_license
2566                .metadata
2567                .get("tier")
2568                .unwrap()
2569                .as_str()
2570                .unwrap(),
2571            "enterprise"
2572        );
2573
2574        let _ = reset_config();
2575    }
2576
2577    #[cfg(feature = "token")]
2578    #[tokio::test]
2579    async fn test_update_license_clear_limits() {
2580        let license = create_test_license();
2581        let _m = mock("PATCH", "/v1/licenses/test_license_id")
2582            .with_status(200)
2583            .with_header("content-type", "application/json")
2584            .with_body(
2585                json!({
2586                    "data": {
2587                        "id": "test_license_id",
2588                        "type": "licenses",
2589                        "attributes": {
2590                            "key": "TEST-LICENSE-KEY",
2591                            "name": "Test License",
2592                            "expiry": null,
2593                            "status": "active",
2594                            "uses": null,
2595                            "maxMachines": null,
2596                            "maxProcesses": null,
2597                            "maxUsers": null,
2598                            "maxCores": null,
2599                            "maxUses": null,
2600                            "protected": null,
2601                            "suspended": false,
2602                            "metadata": {}
2603                        },
2604                        "relationships": {
2605                            "policy": {
2606                                "data": {
2607                                    "type": "policies",
2608                                    "id": "policy-123"
2609                                }
2610                            }
2611                        }
2612                    }
2613                })
2614                .to_string(),
2615            )
2616            .create();
2617
2618        let _ = set_config(KeygenConfig {
2619            api_url: server_url(),
2620            account: "test_account".to_string(),
2621            token: Some("admin-token".to_string()),
2622            ..Default::default()
2623        });
2624
2625        // Test clearing limits (setting them to null)
2626        let request = LicenseUpdateRequest::new()
2627            .clear_max_machines()
2628            .clear_max_processes()
2629            .clear_max_users()
2630            .clear_max_cores()
2631            .clear_max_uses();
2632
2633        let result = license.update(request).await;
2634
2635        assert!(result.is_ok());
2636        let updated_license = result.unwrap();
2637        assert_eq!(updated_license.max_machines, None);
2638        assert_eq!(updated_license.max_processes, None);
2639        assert_eq!(updated_license.max_users, None);
2640        assert_eq!(updated_license.max_cores, None);
2641        assert_eq!(updated_license.max_uses, None);
2642
2643        let _ = reset_config();
2644    }
2645
2646    #[cfg(feature = "token")]
2647    #[tokio::test]
2648    async fn test_update_license_basic() {
2649        let license = create_test_license();
2650        let _m = mock("PATCH", "/v1/licenses/test_license_id")
2651            .with_status(200)
2652            .with_header("content-type", "application/json")
2653            .with_body(
2654                json!({
2655                    "data": {
2656                        "id": "test_license_id",
2657                        "type": "licenses",
2658                        "attributes": {
2659                            "key": "TEST-LICENSE-KEY",
2660                            "name": "Updated Name",
2661                            "expiry": "2025-06-30T23:59:59Z",
2662                            "status": "active",
2663                            "uses": null,
2664                            "maxMachines": null,
2665                            "maxCores": null,
2666                            "maxUses": null,
2667                            "maxProcesses": null,
2668                            "protected": null,
2669                            "suspended": false,
2670                            "metadata": {
2671                                "updated": true
2672                            }
2673                        },
2674                        "relationships": {
2675                            "policy": {
2676                                "data": {
2677                                    "type": "policies",
2678                                    "id": "policy-123"
2679                                }
2680                            }
2681                        }
2682                    }
2683                })
2684                .to_string(),
2685            )
2686            .create();
2687
2688        let _ = set_config(KeygenConfig {
2689            api_url: server_url(),
2690            account: "test_account".to_string(),
2691            token: Some("admin-token".to_string()),
2692            ..Default::default()
2693        });
2694
2695        let expiry = Utc.from_utc_datetime(
2696            &chrono::NaiveDate::from_ymd_opt(2025, 6, 30)
2697                .unwrap()
2698                .and_hms_opt(23, 59, 59)
2699                .unwrap(),
2700        );
2701        let mut metadata = HashMap::new();
2702        metadata.insert("updated".to_string(), json!(true));
2703
2704        let request = LicenseUpdateRequest::new()
2705            .with_name("Updated Name".to_string())
2706            .with_expiry(expiry)
2707            .with_metadata(metadata);
2708
2709        let result = license.update(request).await;
2710
2711        assert!(result.is_ok());
2712        let updated_license = result.unwrap();
2713        assert_eq!(updated_license.id, "test_license_id");
2714        assert_eq!(updated_license.name, Some("Updated Name".to_string()));
2715        assert!(updated_license.metadata.contains_key("updated"));
2716
2717        let _ = reset_config();
2718    }
2719
2720    #[test]
2721    fn test_license_update_request_builder() {
2722        let mut metadata = HashMap::new();
2723        metadata.insert("tier".to_string(), json!("premium"));
2724
2725        let request = LicenseUpdateRequest::new()
2726            .with_name("Test License".to_string())
2727            .with_max_machines(10)
2728            .with_protected(true)
2729            .with_metadata(metadata.clone());
2730
2731        assert_eq!(request.name, Some("Test License".to_string()));
2732        assert!(matches!(request.max_machines, UpdateField::Set(10)));
2733        assert!(request.protected == Some(true));
2734        assert_eq!(request.metadata, Some(metadata));
2735
2736        // Test clearing a limit
2737        let request_with_clear = LicenseUpdateRequest::new()
2738            .with_max_machines(5)
2739            .clear_max_machines();
2740
2741        assert!(matches!(
2742            request_with_clear.max_machines,
2743            UpdateField::Clear
2744        ));
2745
2746        // Test JSON body conversion
2747        let body = request.to_json_body();
2748        let data = body.get("data").unwrap();
2749        let attributes = data.get("attributes").unwrap();
2750        assert_eq!(
2751            attributes.get("name").unwrap().as_str().unwrap(),
2752            "Test License"
2753        );
2754        assert_eq!(attributes.get("maxMachines").unwrap().as_i64().unwrap(), 10);
2755        assert!(attributes.get("protected").unwrap().as_bool().unwrap());
2756        assert!(attributes.get("metadata").is_some());
2757    }
2758
2759    #[cfg(feature = "token")]
2760    #[tokio::test]
2761    async fn test_license_list_pagination_with_page_number() {
2762        let _m = mock("GET", "/v1/licenses")
2763            .match_query(mockito::Matcher::AllOf(vec![
2764                mockito::Matcher::UrlEncoded("page[number]".into(), "2".into()),
2765                mockito::Matcher::UrlEncoded("page[size]".into(), "15".into()),
2766            ]))
2767            .with_status(200)
2768            .with_header("content-type", "application/json")
2769            .with_body(
2770                json!({
2771                    "data": [
2772                        {
2773                            "id": "license-1",
2774                            "type": "licenses",
2775                            "attributes": {
2776                                "key": "TEST-LICENSE-1",
2777                                "name": "Test License 1",
2778                                "expiry": null,
2779                                "status": "active",
2780                                "uses": null,
2781                                "maxMachines": null,
2782                                "maxCores": null,
2783                                "maxUses": null,
2784                                "maxProcesses": null,
2785                                "protected": null,
2786                                "suspended": false,
2787                                "metadata": {}
2788                            },
2789                            "relationships": {
2790                                "policy": {
2791                                    "data": {
2792                                        "type": "policies",
2793                                        "id": "policy-123"
2794                                    }
2795                                }
2796                            }
2797                        }
2798                    ]
2799                })
2800                .to_string(),
2801            )
2802            .create();
2803
2804        let _ = set_config(KeygenConfig {
2805            api_url: server_url(),
2806            account: "test_account".to_string(),
2807            token: Some("admin-token".to_string()),
2808            ..Default::default()
2809        });
2810
2811        let options = LicenseListOptions {
2812            page_number: Some(2),
2813            page_size: Some(15),
2814            ..Default::default()
2815        };
2816
2817        let result = License::list(Some(&options)).await;
2818        assert!(result.is_ok());
2819        let licenses = result.unwrap();
2820        assert_eq!(licenses.len(), 1);
2821        assert_eq!(licenses[0].id, "license-1");
2822
2823        let _ = reset_config();
2824    }
2825
2826    #[cfg(feature = "token")]
2827    #[tokio::test]
2828    async fn test_license_list_pagination_with_limit_only() {
2829        let _m = mock("GET", "/v1/licenses")
2830            .match_query(mockito::Matcher::UrlEncoded("limit".into(), "5".into()))
2831            .with_status(200)
2832            .with_header("content-type", "application/json")
2833            .with_body(
2834                json!({
2835                    "data": []
2836                })
2837                .to_string(),
2838            )
2839            .create();
2840
2841        let _ = set_config(KeygenConfig {
2842            api_url: server_url(),
2843            account: "test_account".to_string(),
2844            token: Some("admin-token".to_string()),
2845            ..Default::default()
2846        });
2847
2848        let options = LicenseListOptions {
2849            limit: Some(5),
2850            ..Default::default()
2851        };
2852
2853        let result = License::list(Some(&options)).await;
2854        assert!(result.is_ok());
2855
2856        let _ = reset_config();
2857    }
2858
2859    #[tokio::test]
2860    async fn test_pagination_options_with_new_parameters() {
2861        let _m = mock("GET", "/v1/licenses/test_license_id/machines")
2862            .match_query(mockito::Matcher::AllOf(vec![
2863                mockito::Matcher::UrlEncoded("page[number]".into(), "3".into()),
2864                mockito::Matcher::UrlEncoded("page[size]".into(), "25".into()),
2865                mockito::Matcher::UrlEncoded("limit".into(), "50".into()),
2866            ]))
2867            .with_status(200)
2868            .with_header("content-type", "application/json")
2869            .with_body(
2870                json!({
2871                    "data": []
2872                })
2873                .to_string(),
2874            )
2875            .create();
2876
2877        let config = KeygenConfig {
2878            api_url: server_url(),
2879            account: "test_account".to_string(),
2880            product: "test_product".to_string(),
2881            ..Default::default()
2882        };
2883        let _ = set_config(config.clone());
2884
2885        let license = create_test_license().with_config(config);
2886        let pagination_options = PaginationOptions {
2887            limit: Some(50),
2888            page_number: Some(3),
2889            page_size: Some(25),
2890        };
2891
2892        let result = license.machines(Some(&pagination_options)).await;
2893        assert!(result.is_ok());
2894        let _ = reset_config();
2895    }
2896
2897    #[cfg(feature = "token")]
2898    #[tokio::test]
2899    async fn test_attach_entitlements() {
2900        let license = create_test_license();
2901        let _m = mock("POST", "/v1/licenses/test_license_id/entitlements")
2902            .with_status(204)
2903            .with_header("content-type", "application/json")
2904            .create();
2905
2906        let _ = set_config(KeygenConfig {
2907            api_url: server_url(),
2908            account: "test_account".to_string(),
2909            token: Some("admin-token".to_string()),
2910            ..Default::default()
2911        });
2912
2913        let entitlement_ids = vec!["entitlement-1".to_string(), "entitlement-2".to_string()];
2914
2915        let result = license.attach_entitlements(&entitlement_ids).await;
2916        assert!(result.is_ok());
2917        let _ = reset_config();
2918    }
2919
2920    #[cfg(feature = "token")]
2921    #[tokio::test]
2922    async fn test_detach_entitlements() {
2923        let license = create_test_license();
2924        let _m = mock("DELETE", "/v1/licenses/test_license_id/entitlements")
2925            .with_status(204)
2926            .with_header("content-type", "application/json")
2927            .create();
2928
2929        let _ = set_config(KeygenConfig {
2930            api_url: server_url(),
2931            account: "test_account".to_string(),
2932            token: Some("admin-token".to_string()),
2933            ..Default::default()
2934        });
2935
2936        let entitlement_ids = vec!["entitlement-1".to_string(), "entitlement-2".to_string()];
2937
2938        let result = license.detach_entitlements(&entitlement_ids).await;
2939        assert!(result.is_ok());
2940        let _ = reset_config();
2941    }
2942
2943    #[cfg(feature = "token")]
2944    #[tokio::test]
2945    async fn test_attach_entitlements_empty_list() {
2946        let license = create_test_license();
2947        let _m = mock("POST", "/v1/licenses/test_license_id/entitlements")
2948            .with_status(204)
2949            .with_header("content-type", "application/json")
2950            .create();
2951
2952        let _ = set_config(KeygenConfig {
2953            api_url: server_url(),
2954            account: "test_account".to_string(),
2955            token: Some("admin-token".to_string()),
2956            ..Default::default()
2957        });
2958
2959        let entitlement_ids: Vec<String> = vec![];
2960        let result = license.attach_entitlements(&entitlement_ids).await;
2961        assert!(result.is_ok());
2962        let _ = reset_config();
2963    }
2964
2965    #[cfg(feature = "token")]
2966    #[tokio::test]
2967    async fn test_detach_entitlements_empty_list() {
2968        let license = create_test_license();
2969        let _m = mock("DELETE", "/v1/licenses/test_license_id/entitlements")
2970            .with_status(204)
2971            .with_header("content-type", "application/json")
2972            .create();
2973
2974        let _ = set_config(KeygenConfig {
2975            api_url: server_url(),
2976            account: "test_account".to_string(),
2977            token: Some("admin-token".to_string()),
2978            ..Default::default()
2979        });
2980
2981        let entitlement_ids: Vec<String> = vec![];
2982        let result = license.detach_entitlements(&entitlement_ids).await;
2983        assert!(result.is_ok());
2984        let _ = reset_config();
2985    }
2986
2987    #[tokio::test]
2988    async fn test_increment_usage() {
2989        let license = create_test_license();
2990        let _m = mock(
2991            "POST",
2992            "/v1/licenses/test_license_id/actions/increment-usage",
2993        )
2994        .with_status(200)
2995        .with_header("content-type", "application/json")
2996        .with_body(
2997            json!({
2998                "data": {
2999                    "id": "test_license_id",
3000                    "type": "licenses",
3001                    "attributes": {
3002                        "key": "TEST-LICENSE-KEY",
3003                        "name": "Test License",
3004                        "expiry": null,
3005                        "status": "active",
3006                        "uses": 6, // Incremented from 5 to 6
3007                        "maxMachines": null,
3008                        "maxCores": null,
3009                        "maxUses": 100,
3010                        "maxProcesses": null,
3011                        "maxUsers": null,
3012                        "protected": null,
3013                        "suspended": false,
3014                        "permissions": null,
3015                        "metadata": {}
3016                    },
3017                    "relationships": {
3018                        "policy": {
3019                            "data": {
3020                                "type": "policies",
3021                                "id": "policy-123"
3022                            }
3023                        }
3024                    }
3025                }
3026            })
3027            .to_string(),
3028        )
3029        .create();
3030
3031        let _ = set_config(KeygenConfig {
3032            api_url: server_url(),
3033            account: "test_account".to_string(),
3034            product: "test_product".to_string(),
3035            license_key: Some("TEST-LICENSE-KEY".to_string()),
3036            ..Default::default()
3037        });
3038
3039        let result = license.increment_usage().await;
3040        assert!(result.is_ok());
3041        let updated_license = result.unwrap();
3042        assert_eq!(updated_license.uses, Some(6));
3043        assert_eq!(updated_license.id, "test_license_id");
3044        let _ = reset_config();
3045    }
3046
3047    #[cfg(feature = "token")]
3048    #[tokio::test]
3049    async fn test_decrement_usage() {
3050        let license = create_test_license();
3051        let _m = mock(
3052            "POST",
3053            "/v1/licenses/test_license_id/actions/decrement-usage",
3054        )
3055        .with_status(200)
3056        .with_header("content-type", "application/json")
3057        .with_body(
3058            json!({
3059                "data": {
3060                    "id": "test_license_id",
3061                    "type": "licenses",
3062                    "attributes": {
3063                        "key": "TEST-LICENSE-KEY",
3064                        "name": "Test License",
3065                        "expiry": null,
3066                        "status": "active",
3067                        "uses": 4, // Decremented from 5 to 4
3068                        "maxMachines": null,
3069                        "maxCores": null,
3070                        "maxUses": 100,
3071                        "maxProcesses": null,
3072                        "maxUsers": null,
3073                        "protected": null,
3074                        "suspended": false,
3075                        "permissions": null,
3076                        "metadata": {}
3077                    },
3078                    "relationships": {
3079                        "policy": {
3080                            "data": {
3081                                "type": "policies",
3082                                "id": "policy-123"
3083                            }
3084                        }
3085                    }
3086                }
3087            })
3088            .to_string(),
3089        )
3090        .create();
3091
3092        let _ = set_config(KeygenConfig {
3093            api_url: server_url(),
3094            account: "test_account".to_string(),
3095            token: Some("admin-token".to_string()),
3096            ..Default::default()
3097        });
3098
3099        let result = license.decrement_usage().await;
3100        assert!(result.is_ok());
3101        let updated_license = result.unwrap();
3102        assert_eq!(updated_license.uses, Some(4));
3103        assert_eq!(updated_license.id, "test_license_id");
3104        let _ = reset_config();
3105    }
3106
3107    #[cfg(feature = "token")]
3108    #[tokio::test]
3109    async fn test_reset_usage() {
3110        let license = create_test_license();
3111        let _m = mock("POST", "/v1/licenses/test_license_id/actions/reset-usage")
3112            .with_status(200)
3113            .with_header("content-type", "application/json")
3114            .with_body(
3115                json!({
3116                    "data": {
3117                        "id": "test_license_id",
3118                        "type": "licenses",
3119                        "attributes": {
3120                            "key": "TEST-LICENSE-KEY",
3121                            "name": "Test License",
3122                            "expiry": null,
3123                            "status": "active",
3124                            "uses": 0, // Reset to 0
3125                            "maxMachines": null,
3126                            "maxCores": null,
3127                            "maxUses": 100,
3128                            "maxProcesses": null,
3129                            "maxUsers": null,
3130                            "protected": null,
3131                            "suspended": false,
3132                            "permissions": null,
3133                            "metadata": {}
3134                        },
3135                        "relationships": {
3136                            "policy": {
3137                                "data": {
3138                                    "type": "policies",
3139                                    "id": "policy-123"
3140                                }
3141                            }
3142                        }
3143                    }
3144                })
3145                .to_string(),
3146            )
3147            .create();
3148
3149        let _ = set_config(KeygenConfig {
3150            api_url: server_url(),
3151            account: "test_account".to_string(),
3152            token: Some("admin-token".to_string()),
3153            ..Default::default()
3154        });
3155
3156        let result = license.reset_usage().await;
3157        assert!(result.is_ok());
3158        let updated_license = result.unwrap();
3159        assert_eq!(updated_license.uses, Some(0));
3160        assert_eq!(updated_license.id, "test_license_id");
3161        let _ = reset_config();
3162    }
3163
3164    #[test]
3165    fn test_scheme_code_serialization() {
3166        assert_eq!(
3167            serde_json::to_string(&SchemeCode::Ed25519Sign).unwrap(),
3168            "\"ED25519_SIGN\""
3169        );
3170        assert_eq!(
3171            serde_json::to_string(&SchemeCode::EcdsaP256Sign).unwrap(),
3172            "\"ECDSA_P256_SIGN\""
3173        );
3174        assert_eq!(
3175            serde_json::to_string(&SchemeCode::Rsa2048Pkcs1PssSignV2).unwrap(),
3176            "\"RSA_2048_PKCS1_PSS_SIGN_V2\""
3177        );
3178        assert_eq!(
3179            serde_json::to_string(&SchemeCode::LegacyEncrypt).unwrap(),
3180            "\"LEGACY_ENCRYPT\""
3181        );
3182    }
3183
3184    #[test]
3185    fn test_scheme_code_deserialization() {
3186        assert_eq!(
3187            serde_json::from_str::<SchemeCode>("\"ED25519_SIGN\"").unwrap(),
3188            SchemeCode::Ed25519Sign
3189        );
3190        assert_eq!(
3191            serde_json::from_str::<SchemeCode>("\"ECDSA_P256_SIGN\"").unwrap(),
3192            SchemeCode::EcdsaP256Sign
3193        );
3194        assert_eq!(
3195            serde_json::from_str::<SchemeCode>("\"RSA_2048_PKCS1_PSS_SIGN_V2\"").unwrap(),
3196            SchemeCode::Rsa2048Pkcs1PssSignV2
3197        );
3198        assert_eq!(
3199            serde_json::from_str::<SchemeCode>("\"LEGACY_ENCRYPT\"").unwrap(),
3200            SchemeCode::LegacyEncrypt
3201        );
3202    }
3203}