1use 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#[derive(Debug, Clone, Default)]
38pub enum UpdateField<T> {
39 #[default]
41 Keep,
42 Clear,
44 Set(T),
46}
47
48impl<T: Serialize> UpdateField<T> {
49 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, #[serde(rename = "RSA_2048_PKCS1_SIGN")]
82 Rsa2048Pkcs1Sign, }
84
85#[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 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 pub fn new() -> Self {
198 Self::default()
199 }
200
201 pub fn with_ttl(ttl: i64) -> Self {
203 Self {
204 ttl: Some(ttl),
205 ..Self::default()
206 }
207 }
208
209 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#[derive(Debug, Default)]
255pub struct LicenseListOptions {
256 pub limit: Option<i32>, pub page_number: Option<i32>, pub page_size: Option<i32>, pub status: Option<String>, pub product: Option<String>, pub policy: Option<String>, pub owner: Option<String>, pub user: Option<String>, 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#[derive(Debug, Default)]
281pub struct LicenseCreateRequest {
282 pub policy_id: String,
284
285 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 pub owner_id: Option<String>, pub group_id: Option<String>, }
303
304#[derive(Debug, Default)]
306pub struct LicenseUpdateRequest {
307 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 pub fn new(policy_id: String) -> Self {
324 Self {
325 policy_id,
326 ..Default::default()
327 }
328 }
329
330 pub fn with_name(mut self, name: String) -> Self {
332 self.name = Some(name);
333 self
334 }
335
336 pub fn with_key(mut self, key: String) -> Self {
338 self.key = Some(key);
339 self
340 }
341
342 pub fn with_expiry(mut self, expiry: DateTime<Utc>) -> Self {
344 self.expiry = Some(expiry);
345 self
346 }
347
348 pub fn with_max_machines(mut self, max_machines: i32) -> Self {
350 self.max_machines = Some(max_machines);
351 self
352 }
353
354 pub fn with_max_processes(mut self, max_processes: i32) -> Self {
356 self.max_processes = Some(max_processes);
357 self
358 }
359
360 pub fn with_max_users(mut self, max_users: i32) -> Self {
362 self.max_users = Some(max_users);
363 self
364 }
365
366 pub fn with_max_cores(mut self, max_cores: i32) -> Self {
368 self.max_cores = Some(max_cores);
369 self
370 }
371
372 pub fn with_max_uses(mut self, max_uses: i32) -> Self {
374 self.max_uses = Some(max_uses);
375 self
376 }
377
378 pub fn with_protected(mut self, protected: bool) -> Self {
380 self.protected = Some(protected);
381 self
382 }
383
384 pub fn with_suspended(mut self, suspended: bool) -> Self {
386 self.suspended = Some(suspended);
387 self
388 }
389
390 pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
392 self.permissions = Some(permissions);
393 self
394 }
395
396 pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
398 self.metadata = Some(metadata);
399 self
400 }
401
402 pub fn with_owner_id(mut self, owner_id: String) -> Self {
404 self.owner_id = Some(owner_id);
405 self
406 }
407
408 pub fn with_group_id(mut self, group_id: String) -> Self {
410 self.group_id = Some(group_id);
411 self
412 }
413
414 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 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 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 pub fn new() -> Self {
482 Self::default()
483 }
484
485 pub fn with_name(mut self, name: String) -> Self {
487 self.name = Some(name);
488 self
489 }
490
491 pub fn with_expiry(mut self, expiry: DateTime<Utc>) -> Self {
493 self.expiry = Some(expiry);
494 self
495 }
496
497 pub fn with_max_machines(mut self, max_machines: i32) -> Self {
499 self.max_machines = UpdateField::Set(max_machines);
500 self
501 }
502
503 pub fn clear_max_machines(mut self) -> Self {
505 self.max_machines = UpdateField::Clear;
506 self
507 }
508
509 pub fn with_max_processes(mut self, max_processes: i32) -> Self {
511 self.max_processes = UpdateField::Set(max_processes);
512 self
513 }
514
515 pub fn clear_max_processes(mut self) -> Self {
517 self.max_processes = UpdateField::Clear;
518 self
519 }
520
521 pub fn with_max_users(mut self, max_users: i32) -> Self {
523 self.max_users = UpdateField::Set(max_users);
524 self
525 }
526
527 pub fn clear_max_users(mut self) -> Self {
529 self.max_users = UpdateField::Clear;
530 self
531 }
532
533 pub fn with_max_cores(mut self, max_cores: i32) -> Self {
535 self.max_cores = UpdateField::Set(max_cores);
536 self
537 }
538
539 pub fn clear_max_cores(mut self) -> Self {
541 self.max_cores = UpdateField::Clear;
542 self
543 }
544
545 pub fn with_max_uses(mut self, max_uses: i32) -> Self {
547 self.max_uses = UpdateField::Set(max_uses);
548 self
549 }
550
551 pub fn clear_max_uses(mut self) -> Self {
553 self.max_uses = UpdateField::Clear;
554 self
555 }
556
557 pub fn with_protected(mut self, protected: bool) -> Self {
559 self.protected = Some(protected);
560 self
561 }
562
563 pub fn with_suspended(mut self, suspended: bool) -> Self {
565 self.suspended = Some(suspended);
566 self
567 }
568
569 pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
571 self.permissions = Some(permissions);
572 self
573 }
574
575 pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
577 self.metadata = Some(metadata);
578 self
579 }
580
581 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 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 pub fn with_config(mut self, config: KeygenConfig) -> Self {
703 self.config = Some(Arc::new(config));
704 self
705 }
706
707 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(¶ms),
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(¶ms), 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(¶ms), 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 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 #[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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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, "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, "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, "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}