use std::env;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use crate::certificate::CertificateFileResponse;
use crate::client::{Client, ClientOptions};
use crate::component::Component;
use crate::config::{get_config, KeygenConfig};
use crate::entitlement::{Entitlement, EntitlementsResponse};
use crate::errors::Error;
use crate::insert_optional;
use crate::license_file::LicenseFile;
use crate::machine::{Machine, MachineResponse, MachinesResponse};
#[cfg(feature = "token")]
use crate::token::{token_request_attributes, CreateTokenRequest, Token, TokenResponse};
#[cfg(feature = "token")]
use crate::user::{User, UserAttributes};
use crate::verifier::Verifier;
use crate::KeygenResponseData;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub enum UpdateField<T> {
#[default]
Keep,
Clear,
Set(T),
}
impl<T: Serialize> UpdateField<T> {
pub fn apply_to(&self, map: &mut serde_json::Map<String, Value>, key: &str) {
match self {
UpdateField::Keep => {}
UpdateField::Clear => {
map.insert(key.to_string(), Value::Null);
}
UpdateField::Set(value) => {
map.insert(key.to_string(), json!(value));
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SchemeCode {
#[serde(rename = "ED25519_SIGN")]
Ed25519Sign,
#[serde(rename = "ECDSA_P256_SIGN")]
EcdsaP256Sign,
#[serde(rename = "RSA_2048_PKCS1_PSS_SIGN_V2")]
Rsa2048Pkcs1PssSignV2,
#[serde(rename = "RSA_2048_PKCS1_SIGN_V2")]
Rsa2048Pkcs1SignV2,
#[serde(rename = "RSA_2048_PKCS1_ENCRYPT")]
Rsa2048Pkcs1Encrypt,
#[serde(rename = "RSA_2048_JWT_RS256")]
Rsa2048JwtRs256,
#[serde(rename = "LEGACY_ENCRYPT")]
LegacyEncrypt,
#[serde(rename = "RSA_2048_PKCS1_PSS_SIGN")]
Rsa2048Pkcs1PssSign, #[serde(rename = "RSA_2048_PKCS1_SIGN")]
Rsa2048Pkcs1Sign, }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum LicenseStatus {
Active,
Inactive,
Expiring,
Expired,
Suspended,
Banned,
}
impl LicenseStatus {
pub fn parse(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"ACTIVE" => Some(Self::Active),
"INACTIVE" => Some(Self::Inactive),
"EXPIRING" => Some(Self::Expiring),
"EXPIRED" => Some(Self::Expired),
"SUSPENDED" => Some(Self::Suspended),
"BANNED" => Some(Self::Banned),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LicenseResponse<M> {
pub meta: Option<M>,
pub data: KeygenResponseData<LicenseAttributes>,
}
#[cfg(feature = "token")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LicenseUsersResponse {
pub data: Vec<KeygenResponseData<UserAttributes>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ValidationMeta {
pub ts: DateTime<Utc>,
pub valid: bool,
pub detail: String,
pub code: String,
pub scope: ValidationScope,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ValidationScope {
pub fingerprint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LicenseAttributes {
pub key: String,
pub name: Option<String>,
pub expiry: Option<DateTime<Utc>>,
pub status: Option<String>,
pub uses: Option<i32>,
#[serde(rename = "maxMachines")]
pub max_machines: Option<i32>,
#[serde(rename = "maxCores")]
pub max_cores: Option<i32>,
#[serde(rename = "maxUses")]
pub max_uses: Option<i32>,
#[serde(rename = "maxProcesses")]
pub max_processes: Option<i32>,
#[serde(rename = "maxUsers")]
pub max_users: Option<i32>,
pub protected: Option<bool>,
pub suspended: Option<bool>,
pub permissions: Option<Vec<String>>,
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct License {
pub id: String,
#[serde(skip_serializing)]
pub scheme: Option<SchemeCode>,
pub key: String,
pub name: Option<String>,
pub expiry: Option<DateTime<Utc>>,
pub status: Option<String>,
pub uses: Option<i32>,
pub max_machines: Option<i32>,
pub max_cores: Option<i32>,
pub max_uses: Option<i32>,
pub max_processes: Option<i32>,
pub max_users: Option<i32>,
pub protected: Option<bool>,
pub suspended: Option<bool>,
pub permissions: Option<Vec<String>>,
pub policy: Option<String>,
pub metadata: HashMap<String, Value>,
pub account_id: Option<String>,
pub product_id: Option<String>,
pub group_id: Option<String>,
pub owner_id: Option<String>,
#[serde(skip)]
pub config: Option<Arc<KeygenConfig>>,
}
#[derive(Debug, Clone, Default)]
pub struct LicenseCheckoutOpts {
pub ttl: Option<i64>,
pub include: Option<Vec<String>>,
}
impl LicenseCheckoutOpts {
pub fn new() -> Self {
Self::default()
}
pub fn with_ttl(ttl: i64) -> Self {
Self {
ttl: Some(ttl),
..Self::default()
}
}
pub fn with_include(include: Vec<String>) -> Self {
Self {
include: Some(include),
..Self::default()
}
}
}
#[derive(Debug, Default, Serialize)]
pub struct PaginationOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i32>,
#[serde(rename = "page[number]", skip_serializing_if = "Option::is_none")]
pub page_number: Option<i32>,
#[serde(rename = "page[size]", skip_serializing_if = "Option::is_none")]
pub page_size: Option<i32>,
}
#[derive(Debug, Clone, Default)]
pub struct NumericFilter {
pub eq: Option<i32>,
pub gt: Option<i32>,
pub gte: Option<i32>,
pub lt: Option<i32>,
pub lte: Option<i32>,
}
#[derive(Debug, Clone, Default)]
pub struct DateWindowFilter {
pub r#in: Option<String>,
pub on: Option<String>,
pub before: Option<String>,
pub after: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct LicenseActivityFilter {
pub inside: Option<String>,
pub outside: Option<String>,
pub before: Option<String>,
pub after: Option<String>,
}
#[derive(Debug, Default)]
pub struct LicenseListOptions {
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>,
pub machine: Option<String>,
pub assigned: Option<bool>,
pub unassigned: Option<bool>,
pub activated: Option<bool>,
pub metadata: Option<HashMap<String, Value>>,
pub activations: Option<NumericFilter>,
pub expires: Option<DateWindowFilter>,
pub expired: Option<DateWindowFilter>,
pub activity: Option<LicenseActivityFilter>,
}
#[derive(Debug, Default)]
pub struct LicenseCreateRequest {
pub policy_id: String,
pub name: Option<String>,
pub key: Option<String>,
pub expiry: Option<DateTime<Utc>>,
pub max_machines: Option<i32>,
pub max_processes: Option<i32>,
pub max_users: Option<i32>,
pub max_cores: Option<i32>,
pub max_uses: Option<i32>,
pub protected: Option<bool>,
pub suspended: Option<bool>,
pub permissions: Option<Vec<String>>,
pub metadata: Option<HashMap<String, Value>>,
pub owner_id: Option<String>, pub group_id: Option<String>, }
#[derive(Debug, Default)]
pub struct LicenseUpdateRequest {
pub name: Option<String>,
pub expiry: Option<DateTime<Utc>>,
pub max_machines: UpdateField<i32>,
pub max_processes: UpdateField<i32>,
pub max_users: UpdateField<i32>,
pub max_cores: UpdateField<i32>,
pub max_uses: UpdateField<i32>,
pub protected: Option<bool>,
pub suspended: Option<bool>,
pub permissions: Option<Vec<String>>,
pub metadata: Option<HashMap<String, Value>>,
}
impl LicenseCreateRequest {
pub fn new(policy_id: String) -> Self {
Self {
policy_id,
..Default::default()
}
}
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
pub fn with_key(mut self, key: String) -> Self {
self.key = Some(key);
self
}
pub fn with_expiry(mut self, expiry: DateTime<Utc>) -> Self {
self.expiry = Some(expiry);
self
}
pub fn with_max_machines(mut self, max_machines: i32) -> Self {
self.max_machines = Some(max_machines);
self
}
pub fn with_max_processes(mut self, max_processes: i32) -> Self {
self.max_processes = Some(max_processes);
self
}
pub fn with_max_users(mut self, max_users: i32) -> Self {
self.max_users = Some(max_users);
self
}
pub fn with_max_cores(mut self, max_cores: i32) -> Self {
self.max_cores = Some(max_cores);
self
}
pub fn with_max_uses(mut self, max_uses: i32) -> Self {
self.max_uses = Some(max_uses);
self
}
pub fn with_protected(mut self, protected: bool) -> Self {
self.protected = Some(protected);
self
}
pub fn with_suspended(mut self, suspended: bool) -> Self {
self.suspended = Some(suspended);
self
}
pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
self.permissions = Some(permissions);
self
}
pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_owner_id(mut self, owner_id: String) -> Self {
self.owner_id = Some(owner_id);
self
}
pub fn with_group_id(mut self, group_id: String) -> Self {
self.group_id = Some(group_id);
self
}
pub fn to_json_body(self) -> Value {
let mut attributes = serde_json::Map::new();
let mut relationships = serde_json::Map::new();
let _ = insert_optional(&mut attributes, "name", self.name);
let _ = insert_optional(&mut attributes, "key", self.key);
let _ = insert_optional(&mut attributes, "expiry", self.expiry);
let _ = insert_optional(&mut attributes, "maxMachines", self.max_machines);
let _ = insert_optional(&mut attributes, "maxProcesses", self.max_processes);
let _ = insert_optional(&mut attributes, "maxUsers", self.max_users);
let _ = insert_optional(&mut attributes, "maxCores", self.max_cores);
let _ = insert_optional(&mut attributes, "maxUses", self.max_uses);
let _ = insert_optional(&mut attributes, "protected", self.protected);
let _ = insert_optional(&mut attributes, "suspended", self.suspended);
let _ = insert_optional(&mut attributes, "permissions", self.permissions);
let _ = insert_optional(&mut attributes, "metadata", self.metadata);
relationships.insert(
"policy".to_string(),
json!({
"data": {
"type": "policies",
"id": self.policy_id
}
}),
);
if let Some(owner_id) = self.owner_id {
relationships.insert(
"owner".to_string(),
json!({
"data": {
"type": "users",
"id": owner_id
}
}),
);
}
if let Some(group_id) = self.group_id {
relationships.insert(
"group".to_string(),
json!({
"data": {
"type": "groups",
"id": group_id
}
}),
);
}
json!({
"data": {
"type": "licenses",
"attributes": attributes,
"relationships": relationships
}
})
}
}
impl LicenseUpdateRequest {
pub fn new() -> Self {
Self::default()
}
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
pub fn with_expiry(mut self, expiry: DateTime<Utc>) -> Self {
self.expiry = Some(expiry);
self
}
pub fn with_max_machines(mut self, max_machines: i32) -> Self {
self.max_machines = UpdateField::Set(max_machines);
self
}
pub fn clear_max_machines(mut self) -> Self {
self.max_machines = UpdateField::Clear;
self
}
pub fn with_max_processes(mut self, max_processes: i32) -> Self {
self.max_processes = UpdateField::Set(max_processes);
self
}
pub fn clear_max_processes(mut self) -> Self {
self.max_processes = UpdateField::Clear;
self
}
pub fn with_max_users(mut self, max_users: i32) -> Self {
self.max_users = UpdateField::Set(max_users);
self
}
pub fn clear_max_users(mut self) -> Self {
self.max_users = UpdateField::Clear;
self
}
pub fn with_max_cores(mut self, max_cores: i32) -> Self {
self.max_cores = UpdateField::Set(max_cores);
self
}
pub fn clear_max_cores(mut self) -> Self {
self.max_cores = UpdateField::Clear;
self
}
pub fn with_max_uses(mut self, max_uses: i32) -> Self {
self.max_uses = UpdateField::Set(max_uses);
self
}
pub fn clear_max_uses(mut self) -> Self {
self.max_uses = UpdateField::Clear;
self
}
pub fn with_protected(mut self, protected: bool) -> Self {
self.protected = Some(protected);
self
}
pub fn with_suspended(mut self, suspended: bool) -> Self {
self.suspended = Some(suspended);
self
}
pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
self.permissions = Some(permissions);
self
}
pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
self.metadata = Some(metadata);
self
}
pub fn to_json_body(self) -> Value {
let mut attributes = serde_json::Map::new();
if let Some(name) = self.name {
attributes.insert("name".to_string(), json!(name));
}
if let Some(expiry) = self.expiry {
attributes.insert("expiry".to_string(), json!(expiry));
}
self.max_machines.apply_to(&mut attributes, "maxMachines");
self.max_processes.apply_to(&mut attributes, "maxProcesses");
self.max_users.apply_to(&mut attributes, "maxUsers");
self.max_cores.apply_to(&mut attributes, "maxCores");
self.max_uses.apply_to(&mut attributes, "maxUses");
if let Some(protected) = self.protected {
attributes.insert("protected".to_string(), json!(protected));
}
if let Some(suspended) = self.suspended {
attributes.insert("suspended".to_string(), json!(suspended));
}
if let Some(permissions) = self.permissions {
attributes.insert("permissions".to_string(), json!(permissions));
}
if let Some(metadata) = self.metadata {
attributes.insert("metadata".to_string(), json!(metadata));
}
json!({
"data": {
"type": "licenses",
"attributes": attributes
}
})
}
}
impl License {
pub(crate) fn from(data: KeygenResponseData<LicenseAttributes>) -> License {
License {
id: data.id,
scheme: None,
key: data.attributes.key,
name: data.attributes.name,
expiry: data.attributes.expiry,
status: data.attributes.status,
uses: data.attributes.uses,
max_machines: data.attributes.max_machines,
max_cores: data.attributes.max_cores,
max_uses: data.attributes.max_uses,
max_processes: data.attributes.max_processes,
max_users: data.attributes.max_users,
protected: data.attributes.protected,
suspended: data.attributes.suspended,
permissions: data.attributes.permissions,
policy: data.relationships.policy_id(),
metadata: data.attributes.metadata,
account_id: data.relationships.account_id(),
product_id: data.relationships.product_id(),
group_id: data.relationships.group_id(),
owner_id: data.relationships.owner_id(),
config: None,
}
}
pub(crate) fn from_signed_key(scheme: SchemeCode, signed_key: &str) -> License {
License {
id: String::new(),
scheme: Some(scheme),
key: signed_key.to_string(),
name: None,
expiry: None,
status: None,
uses: None,
max_machines: None,
max_cores: None,
max_uses: None,
max_processes: None,
max_users: None,
protected: None,
suspended: None,
permissions: None,
policy: None,
metadata: HashMap::new(),
account_id: None,
product_id: None,
group_id: None,
owner_id: None,
config: None,
}
}
pub fn from_key(key: &str) -> Self {
License {
id: String::new(),
scheme: None,
key: key.to_string(),
name: None,
expiry: None,
status: None,
uses: None,
max_machines: None,
max_cores: None,
max_uses: None,
max_processes: None,
max_users: None,
protected: None,
suspended: None,
permissions: None,
policy: None,
metadata: HashMap::new(),
account_id: None,
product_id: None,
group_id: None,
owner_id: None,
config: None,
}
}
pub fn with_config(mut self, config: KeygenConfig) -> Self {
self.config = Some(Arc::new(config));
self
}
fn get_client(&self) -> Result<Client, Error> {
let config = if let Some(ref cfg) = self.config {
cfg.as_ref().clone()
} else {
get_config()?
};
Client::new(ClientOptions::from(config))
}
fn build_scope(
config: &KeygenConfig,
fingerprints: &[String],
entitlements: &[String],
) -> Result<Value, Error> {
let mut scope = json!({
"product": config.product.to_string(),
});
if !fingerprints.is_empty() {
scope["fingerprint"] = json!(fingerprints[0]);
if fingerprints.len() > 1 {
scope["components"] = json!(fingerprints[1..].to_vec());
}
}
if !entitlements.is_empty() {
scope["entitlements"] = json!(entitlements);
}
if let Some(env) = config.environment.as_ref() {
scope["environment"] = json!(env);
}
Ok(scope)
}
pub async fn validate(
self,
fingerprints: &[String],
entitlements: &[String],
) -> Result<License, Error> {
let client = self.get_client()?;
let config = if let Some(ref cfg) = self.config {
cfg.as_ref()
} else {
&get_config()?
};
let scope = Self::build_scope(config, fingerprints, entitlements)?;
let params = json!({
"meta": {
"nonce": chrono::Utc::now().timestamp(),
"scope": scope
}
});
let response = client
.post(
&format!("licenses/{}/actions/validate", self.id),
Some(¶ms),
None::<&()>,
)
.await?;
let validation: LicenseResponse<ValidationMeta> = serde_json::from_value(response.body)?;
let meta = validation.meta.clone().unwrap();
if !meta.valid {
return Err(self.handle_validation_code(&meta));
};
let license = License::from(validation.data);
Ok(if let Some(cfg) = self.config {
license.with_config((*cfg).clone())
} else {
license
})
}
pub async fn validate_key(
self,
fingerprints: &[String],
entitlements: &[String],
) -> Result<License, Error> {
let client = self.get_client()?;
let config = if let Some(ref cfg) = self.config {
cfg.as_ref()
} else {
&get_config()?
};
let scope = Self::build_scope(config, fingerprints, entitlements)?;
let params = json!({
"meta": {
"key": self.key.clone(),
"scope": scope
}
});
let response = client
.post("licenses/actions/validate-key", Some(¶ms), None::<&()>)
.await?;
let validation: LicenseResponse<ValidationMeta> = serde_json::from_value(response.body)?;
let meta = validation.meta.clone().unwrap();
if !meta.valid {
return Err(self.handle_validation_code(&meta));
};
let license = License::from(validation.data);
Ok(if let Some(cfg) = self.config {
license.with_config((*cfg).clone())
} else {
license
})
}
#[must_use = "verification result should be checked"]
pub fn verify(&self) -> Result<Vec<u8>, Error> {
if self.scheme.is_none() {
return Err(Error::LicenseNotSigned);
}
let config = if let Some(ref cfg) = self.config {
cfg.as_ref().clone()
} else {
get_config()?
};
if let Some(public_key) = &config.public_key {
let verifier = Verifier::new(public_key.clone());
verifier.verify_license(self)
} else {
Err(Error::PublicKeyMissing)
}
}
pub async fn activate(
&self,
fingerprint: &str,
components: &[Component],
) -> Result<Machine, Error> {
#[cfg(not(target_arch = "wasm32"))]
let hostname = hostname::get()
.map(|h| h.to_string_lossy().into_owned())
.unwrap_or_else(|_| String::from("unknown"));
#[cfg(target_arch = "wasm32")]
let hostname = String::from("wasm");
let config = if let Some(ref cfg) = self.config {
cfg.as_ref()
} else {
&get_config()?
};
let platform = config
.platform
.clone()
.or_else(|| Some(format!("{}/{}", env::consts::OS, env::consts::ARCH)));
#[cfg(not(target_arch = "wasm32"))]
let cores = num_cpus::get();
#[cfg(target_arch = "wasm32")]
let cores = 0usize;
let mut params = json!({
"data": {
"type": "machines",
"attributes": {
"fingerprint": fingerprint,
"cores": cores,
"hostname": hostname,
"platform": platform,
},
"relationships": {
"license": {
"data": {
"type": "licenses",
"id": self.id
}
},
}
}
});
if !components.is_empty() {
params["data"]["relationships"]["components"] = json!({
"data": components
.iter()
.map(|comp| json!({
"type": "components",
"attributes": {
"fingerprint": comp.fingerprint,
"name": comp.name
}
}))
.collect::<Vec<serde_json::Value>>()
});
}
let client = self.get_client()?;
let response = client.post("machines", Some(¶ms), None::<&()>).await?;
let machine_response: MachineResponse = serde_json::from_value(response.body)?;
let machine = Machine::from(machine_response.data);
Ok(machine)
}
pub async fn deactivate(&self, id: &str) -> Result<(), Error> {
let client = self.get_client()?;
let _response = client
.delete::<(), serde_json::Value>(&format!("machines/{id}"), None::<&()>)
.await?;
Ok(())
}
pub async fn machine(&self, id: &str) -> Result<Machine, Error> {
let client = self.get_client()?;
let response = client.get(&format!("machines/{id}"), None::<&()>).await?;
let machine_response: MachineResponse = serde_json::from_value(response.body)?;
let machine = Machine::from(machine_response.data).with_config(
self.config
.as_ref()
.ok_or(Error::MissingConfiguration)?
.as_ref()
.clone(),
);
Ok(machine)
}
pub async fn machines(
&self,
options: Option<&PaginationOptions>,
) -> Result<Vec<Machine>, Error> {
let mut query = json!({});
if let Some(opts) = options {
if let Some(limit) = opts.limit {
query["limit"] = json!(limit);
} else {
query["limit"] = json!(100);
}
if let Some(page_number) = opts.page_number {
query["page[number]"] = json!(page_number);
}
if let Some(page_size) = opts.page_size {
query["page[size]"] = json!(page_size);
}
} else {
query["limit"] = json!(100);
}
let client = self.get_client()?;
let response = client
.get(&format!("licenses/{}/machines", self.id), Some(&query))
.await?;
let machines_response: MachinesResponse = serde_json::from_value(response.body)?;
let config = self
.config
.as_ref()
.ok_or(Error::MissingConfiguration)?
.as_ref()
.clone();
let machines = machines_response
.data
.iter()
.map(|d| Machine::from(d.clone()).with_config(config.clone()))
.collect();
Ok(machines)
}
pub async fn entitlements(
&self,
options: Option<&PaginationOptions>,
) -> Result<Vec<Entitlement>, Error> {
let mut query = json!({});
if let Some(opts) = options {
if let Some(limit) = opts.limit {
query["limit"] = json!(limit);
} else {
query["limit"] = json!(100);
}
if let Some(page_number) = opts.page_number {
query["page[number]"] = json!(page_number);
}
if let Some(page_size) = opts.page_size {
query["page[size]"] = json!(page_size);
}
} else {
query["limit"] = json!(100);
}
let client = self.get_client()?;
let response = client
.get(&format!("licenses/{}/entitlements", self.id), Some(&query))
.await?;
let entitlements_response: EntitlementsResponse = serde_json::from_value(response.body)?;
let entitlements = entitlements_response
.data
.iter()
.map(|d| Entitlement::from(d.clone()))
.collect();
Ok(entitlements)
}
pub async fn checkout(&self, options: &LicenseCheckoutOpts) -> Result<LicenseFile, Error> {
let mut query = json!({
"encrypt": 1,
});
if let Some(ttl) = options.ttl {
query["ttl"] = ttl.into();
}
if let Some(ref include) = options.include {
query["include"] = json!(include.join(","));
} else {
query["include"] = "entitlements".into();
}
let client = self.get_client()?;
let response = client
.post(
&format!("licenses/{}/actions/check-out", self.id),
None::<&()>,
Some(&query),
)
.await?;
let license_file_response: CertificateFileResponse = serde_json::from_value(response.body)?;
let license_file = LicenseFile::from(license_file_response.data);
Ok(license_file)
}
pub async fn check_in(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/check-in", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
fn handle_validation_code(&self, meta: &ValidationMeta) -> Error {
let code = meta.code.clone();
let detail = meta.detail.clone();
match code.as_str() {
"FINGERPRINT_SCOPE_MISMATCH" | "NO_MACHINES" | "NO_MACHINE" => {
Error::LicenseNotActivated {
code,
detail,
license: Box::new(self.clone()),
}
}
"EXPIRED" => Error::LicenseExpired { code, detail },
"SUSPENDED" => Error::LicenseSuspended { code, detail },
"TOO_MANY_MACHINES" => Error::LicenseTooManyMachines { code, detail },
"TOO_MANY_CORES" => Error::LicenseTooManyCores { code, detail },
"TOO_MANY_PROCESSES" => Error::LicenseTooManyProcesses { code, detail },
"FINGERPRINT_SCOPE_REQUIRED" | "FINGERPRINT_SCOPE_EMPTY" => {
Error::ValidationFingerprintMissing { code, detail }
}
"COMPONENTS_SCOPE_REQUIRED" | "COMPONENTS_SCOPE_EMPTY" => {
Error::ValidationComponentsMissing { code, detail }
}
"COMPONENTS_SCOPE_MISMATCH" => Error::ComponentNotActivated { code, detail },
"HEARTBEAT_NOT_STARTED" => Error::HeartbeatRequired { code, detail },
"HEARTBEAT_DEAD" => Error::HeartbeatDead { code, detail },
"PRODUCT_SCOPE_REQUIRED" | "PRODUCT_SCOPE_EMPTY" => {
Error::ValidationProductMissing { code, detail }
}
_ => Error::LicenseKeyInvalid { code, detail },
}
}
#[cfg(feature = "token")]
pub async fn create(request: LicenseCreateRequest) -> Result<License, Error> {
let config = get_config()?;
let client = Client::new(ClientOptions::from(config))?;
let body = request.to_json_body();
let response = client.post("licenses", Some(&body), None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn list(options: Option<&LicenseListOptions>) -> Result<Vec<License>, Error> {
let config = get_config()?;
let client = Client::new(ClientOptions::from(config))?;
let mut query = json!({});
if let Some(opts) = options {
if let Some(limit) = opts.limit {
query["limit"] = json!(limit);
}
if let Some(page_number) = opts.page_number {
query["page[number]"] = json!(page_number);
}
if let Some(page_size) = opts.page_size {
query["page[size]"] = json!(page_size);
}
if let Some(ref status) = opts.status {
query["status"] = json!(status);
}
if let Some(ref product) = opts.product {
query["product"] = json!(product);
}
if let Some(ref policy) = opts.policy {
query["policy"] = json!(policy);
}
if let Some(ref owner) = opts.owner {
query["owner"] = json!(owner);
}
if let Some(ref user) = opts.user {
query["user"] = json!(user);
}
if let Some(ref group) = opts.group {
query["group"] = json!(group);
}
if let Some(ref machine) = opts.machine {
query["machine"] = json!(machine);
}
if let Some(assigned) = opts.assigned {
query["assigned"] = json!(assigned);
}
if let Some(unassigned) = opts.unassigned {
query["unassigned"] = json!(unassigned);
}
if let Some(activated) = opts.activated {
query["activated"] = json!(activated);
}
if let Some(ref metadata) = opts.metadata {
for (key, value) in metadata {
if let Some(obj) = query.as_object_mut() {
obj.insert(format!("metadata[{key}]"), value.clone());
}
}
}
if let Some(ref activations) = opts.activations {
if let Some(eq) = activations.eq {
query["activations[eq]"] = json!(eq);
}
if let Some(gt) = activations.gt {
query["activations[gt]"] = json!(gt);
}
if let Some(gte) = activations.gte {
query["activations[gte]"] = json!(gte);
}
if let Some(lt) = activations.lt {
query["activations[lt]"] = json!(lt);
}
if let Some(lte) = activations.lte {
query["activations[lte]"] = json!(lte);
}
}
if let Some(ref expires) = opts.expires {
if let Some(value) = &expires.r#in {
query["expires[in]"] = json!(value);
}
if let Some(value) = &expires.on {
query["expires[on]"] = json!(value);
}
if let Some(value) = &expires.before {
query["expires[before]"] = json!(value);
}
if let Some(value) = &expires.after {
query["expires[after]"] = json!(value);
}
}
if let Some(ref expired) = opts.expired {
if let Some(value) = &expired.r#in {
query["expired[in]"] = json!(value);
}
if let Some(value) = &expired.on {
query["expired[on]"] = json!(value);
}
if let Some(value) = &expired.before {
query["expired[before]"] = json!(value);
}
if let Some(value) = &expired.after {
query["expired[after]"] = json!(value);
}
}
if let Some(ref activity) = opts.activity {
if let Some(value) = &activity.inside {
query["activity[inside]"] = json!(value);
}
if let Some(value) = &activity.outside {
query["activity[outside]"] = json!(value);
}
if let Some(value) = &activity.before {
query["activity[before]"] = json!(value);
}
if let Some(value) = &activity.after {
query["activity[after]"] = json!(value);
}
}
}
let response = client.get("licenses", Some(&query)).await?;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LicensesResponse {
pub data: Vec<KeygenResponseData<LicenseAttributes>>,
}
let licenses_response: LicensesResponse = serde_json::from_value(response.body)?;
Ok(licenses_response
.data
.into_iter()
.map(License::from)
.collect())
}
#[cfg(feature = "token")]
pub async fn get(id: &str) -> Result<License, Error> {
let config = get_config()?;
let client = Client::new(ClientOptions::from(config))?;
let endpoint = format!("licenses/{id}");
let response = client.get(&endpoint, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn update(&self, request: LicenseUpdateRequest) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}", self.id);
let body = request.to_json_body();
let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn delete(&self) -> Result<(), Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}", self.id);
client.delete::<(), ()>(&endpoint, None::<&()>).await?;
Ok(())
}
#[cfg(feature = "token")]
pub async fn suspend(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/suspend", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn reinstate(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/reinstate", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn renew(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/renew", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn revoke(&self) -> Result<(), Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/revoke", self.id);
client.delete::<(), ()>(&endpoint, None::<&()>).await?;
Ok(())
}
pub async fn increment_usage(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/increment-usage", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn decrement_usage(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/decrement-usage", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn reset_usage(&self) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/actions/reset-usage", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn attach_entitlements(&self, entitlement_ids: &[String]) -> Result<(), Error> {
let client = Client::from_global_config()?;
let endpoint = format!("licenses/{}/entitlements", self.id);
let data: Vec<Value> = entitlement_ids
.iter()
.map(|id| {
json!({
"type": "entitlements",
"id": id
})
})
.collect();
let body = json!({
"data": data
});
client
.post::<Value, Value, ()>(&endpoint, Some(&body), None::<&()>)
.await?;
Ok(())
}
#[cfg(feature = "token")]
pub async fn detach_entitlements(&self, entitlement_ids: &[String]) -> Result<(), Error> {
let client = Client::from_global_config()?;
let endpoint = format!("licenses/{}/entitlements", self.id);
let data: Vec<Value> = entitlement_ids
.iter()
.map(|id| {
json!({
"type": "entitlements",
"id": id
})
})
.collect();
let body = json!({
"data": data
});
client
.delete::<Value, Value>(&endpoint, Some(&body))
.await?;
Ok(())
}
#[cfg(feature = "token")]
pub async fn generate_token(
&self,
request: Option<CreateTokenRequest>,
) -> Result<Token, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/tokens", self.id);
let attributes = token_request_attributes(request.as_ref())?;
let body = json!({
"data": {
"type": "tokens",
"attributes": attributes
}
});
let response = client.post(&endpoint, Some(&body), None::<&()>).await?;
let token_response: TokenResponse = serde_json::from_value(response.body)?;
Ok(Token::from(token_response.data))
}
#[cfg(feature = "token")]
pub async fn attach_users(&self, user_ids: &[String]) -> Result<(), Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/users", self.id);
let data: Vec<Value> = user_ids
.iter()
.map(|id| {
json!({
"type": "users",
"id": id
})
})
.collect();
let body = json!({ "data": data });
client
.post::<Value, Value, ()>(&endpoint, Some(&body), None::<&()>)
.await?;
Ok(())
}
#[cfg(feature = "token")]
pub async fn detach_users(&self, user_ids: &[String]) -> Result<(), Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/users", self.id);
let data: Vec<Value> = user_ids
.iter()
.map(|id| {
json!({
"type": "users",
"id": id
})
})
.collect();
let body = json!({ "data": data });
client
.delete::<Value, Value>(&endpoint, Some(&body))
.await?;
Ok(())
}
#[cfg(feature = "token")]
pub async fn users(&self, options: Option<&PaginationOptions>) -> Result<Vec<User>, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/users", self.id);
let response = client.get(&endpoint, options).await?;
let users_response: LicenseUsersResponse = serde_json::from_value(response.body)?;
Ok(users_response.data.into_iter().map(User::from).collect())
}
#[cfg(feature = "token")]
pub async fn change_policy(&self, policy_id: &str) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/policy", self.id);
let body = json!({
"data": {
"type": "policies",
"id": policy_id
}
});
let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn change_owner(&self, owner_id: &str) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/owner", self.id);
let body = json!({
"data": {
"type": "users",
"id": owner_id
}
});
let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
#[cfg(feature = "token")]
pub async fn change_group(&self, group_id: &str) -> Result<License, Error> {
let client = self.get_client()?;
let endpoint = format!("licenses/{}/group", self.id);
let body = json!({
"data": {
"type": "groups",
"id": group_id
}
});
let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
let license_response: LicenseResponse<()> = serde_json::from_value(response.body)?;
Ok(License::from(license_response.data))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{reset_config, set_config, KeygenConfig};
#[cfg(feature = "token")]
use chrono::TimeZone;
use mockito::{mock, server_url};
use serde_json::json;
fn create_test_license() -> License {
License {
id: "test_license_id".to_string(),
scheme: None,
name: Some("Test License".to_string()),
key: "TEST-LICENSE-KEY".to_string(),
expiry: None,
status: None,
uses: None,
max_machines: None,
max_cores: None,
max_uses: None,
max_processes: None,
max_users: None,
protected: None,
suspended: None,
permissions: None,
policy: None,
metadata: HashMap::new(),
account_id: None,
product_id: None,
group_id: None,
owner_id: None,
config: None,
}
}
fn get_mock_body() -> String {
json!({
"meta": {
"ts": "2021-01-01T00:00:00Z",
"valid": true,
"detail": "is valid",
"code": "VALID",
"scope": {
"fingerprint": "test_fingerprint",
"components": ["comp1", "comp2"],
"product": "test_product"
}
},
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"name": "Test License",
"key": "TEST-LICENSE-KEY",
"expiry": null,
"status": "valid",
"metadata": {
"customer_name": "Test Customer",
"customer_email": "test@example.com",
"is_premium": true
}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "11314277-0f31-4a77-9366-0299e9f52123"
}
}
}
}
})
.to_string()
}
#[tokio::test]
async fn test_validate() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(get_mock_body())
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let result = license
.validate(
&[
"test_fingerprint".to_string(),
"comp1".to_string(),
"comp2".to_string(),
],
&[],
)
.await;
assert!(result.is_ok());
let _ = reset_config();
}
#[tokio::test]
async fn test_validate_key() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/actions/validate-key")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(get_mock_body())
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
license_key: Some("TEST-LICENSE-KEY".to_string()),
..Default::default()
});
let result = license
.validate_key(
&[
"test_fingerprint".to_string(),
"comp1".to_string(),
"comp2".to_string(),
],
&[],
)
.await;
assert!(result.is_ok());
let _ = reset_config();
}
#[test]
fn test_verify() {
let mut license = create_test_license();
license.scheme = Some(SchemeCode::Ed25519Sign);
let result = license.verify();
assert!(matches!(result, Err(Error::PublicKeyMissing)));
license.scheme = None;
let result = license.verify();
assert!(matches!(result, Err(Error::LicenseNotSigned)));
}
#[tokio::test]
async fn test_validate_with_metadata() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(get_mock_body())
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let result = license
.validate(
&[
"test_fingerprint".to_string(),
"comp1".to_string(),
"comp2".to_string(),
],
&[],
)
.await;
assert!(result.is_ok());
let validated_license = result.unwrap();
assert!(validated_license.metadata.contains_key("customer_name"));
assert_eq!(
validated_license
.metadata
.get("customer_name")
.unwrap()
.as_str()
.unwrap(),
"Test Customer"
);
assert!(validated_license.metadata.contains_key("customer_email"));
assert_eq!(
validated_license
.metadata
.get("customer_email")
.unwrap()
.as_str()
.unwrap(),
"test@example.com"
);
assert!(validated_license.metadata.contains_key("is_premium"));
assert!(validated_license
.metadata
.get("is_premium")
.unwrap()
.as_bool()
.unwrap());
let _ = reset_config();
}
#[tokio::test]
async fn test_pagination_options() {
let _m = mock("GET", "/v1/licenses/test_license_id/machines")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("limit".into(), "50".into()),
mockito::Matcher::UrlEncoded("page[number]".into(), "2".into()),
mockito::Matcher::UrlEncoded("page[size]".into(), "10".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": []
})
.to_string(),
)
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let config = KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
};
let license = create_test_license().with_config(config);
let pagination_options = PaginationOptions {
limit: Some(50),
page_number: Some(2),
page_size: Some(10),
};
let result = license.machines(Some(&pagination_options)).await;
match &result {
Ok(_) => println!("Test passed"),
Err(e) => println!("Test failed with error: {:?}", e),
}
assert!(result.is_ok());
let _ = reset_config();
}
#[tokio::test]
async fn test_validation_errors() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"meta": {
"ts": "2021-01-01T00:00:00Z",
"valid": false,
"detail": "license expired",
"code": "EXPIRED",
"scope": {
"fingerprint": "test_fingerprint"
}
},
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Test License",
"expiry": null,
"status": "expired",
"uses": null,
"maxMachines": null,
"maxCores": null,
"maxUses": null,
"maxProcesses": null,
"protected": null,
"suspended": null,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy_123"
}
}
}
}
})
.to_string(),
)
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let result = license
.validate(&["test_fingerprint".to_string()], &[])
.await;
assert!(matches!(result, Err(Error::LicenseExpired { .. })));
let _ = reset_config();
}
#[tokio::test]
async fn test_validation_with_empty_scope() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(get_mock_body())
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let result = license.validate(&[], &[]).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[tokio::test]
async fn test_license_with_all_attributes() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/actions/validate")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"meta": {
"ts": "2021-01-01T00:00:00Z",
"valid": true,
"detail": "is valid",
"code": "VALID",
"scope": {
"fingerprint": "test_fingerprint"
}
},
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Test License",
"expiry": "2025-12-31T23:59:59Z",
"status": "active",
"uses": 5,
"maxMachines": 10,
"maxCores": 20,
"maxUses": 100,
"maxProcesses": 5,
"protected": true,
"suspended": false,
"metadata": {
"tier": "premium",
"features": ["feature_a", "feature_b"]
}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy_123"
}
}
}
}
})
.to_string(),
)
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let result = license
.validate(&["test_fingerprint".to_string()], &[])
.await;
assert!(result.is_ok());
let validated_license = result.unwrap();
assert_eq!(validated_license.uses, Some(5));
assert_eq!(validated_license.max_machines, Some(10));
assert_eq!(validated_license.max_cores, Some(20));
assert_eq!(validated_license.max_uses, Some(100));
assert_eq!(validated_license.max_processes, Some(5));
assert!(validated_license.protected == Some(true));
assert_eq!(validated_license.suspended, Some(false));
assert!(validated_license.metadata.contains_key("tier"));
assert_eq!(
validated_license
.metadata
.get("tier")
.unwrap()
.as_str()
.unwrap(),
"premium"
);
let _ = reset_config();
}
#[tokio::test]
async fn test_machine_activation_errors() {
let license = create_test_license();
let _m = mock("POST", "/v1/machines")
.with_status(422)
.with_header("content-type", "application/json")
.with_body(
json!({
"errors": [{
"title": "Unprocessable Entity",
"detail": "License has reached machine limit",
"code": "MACHINE_LIMIT_EXCEEDED"
}]
})
.to_string(),
)
.create();
set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
})
.unwrap();
let result = license.activate("test_fingerprint", &[]).await;
assert!(result.is_err());
let _ = reset_config();
}
#[test]
fn test_license_relationships() {
use crate::{
KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
};
let license_data = KeygenResponseData {
id: "test-license-id".to_string(),
r#type: "licenses".to_string(),
attributes: LicenseAttributes {
key: "TEST-LICENSE-KEY".to_string(),
name: Some("Test License".to_string()),
expiry: None,
status: Some("active".to_string()),
uses: Some(5),
max_machines: Some(10),
max_cores: Some(20),
max_uses: Some(100),
max_processes: Some(5),
max_users: None,
protected: Some(true),
suspended: Some(false),
permissions: None,
metadata: HashMap::new(),
},
relationships: KeygenRelationships {
policy: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "policies".to_string(),
id: "test-policy-id".to_string(),
}),
links: None,
}),
account: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "accounts".to_string(),
id: "test-account-id".to_string(),
}),
links: None,
}),
product: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "products".to_string(),
id: "test-product-id".to_string(),
}),
links: None,
}),
group: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "groups".to_string(),
id: "test-group-id".to_string(),
}),
links: None,
}),
owner: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "users".to_string(),
id: "test-owner-id".to_string(),
}),
links: None,
}),
users: None,
machines: None,
environment: None,
license: None,
release: None,
other: HashMap::new(),
},
};
let license = License::from(license_data);
assert_eq!(license.policy, Some("test-policy-id".to_string()));
assert_eq!(license.account_id, Some("test-account-id".to_string()));
assert_eq!(license.product_id, Some("test-product-id".to_string()));
assert_eq!(license.group_id, Some("test-group-id".to_string()));
assert_eq!(license.owner_id, Some("test-owner-id".to_string()));
assert_eq!(license.id, "test-license-id");
assert_eq!(license.key, "TEST-LICENSE-KEY");
}
#[test]
fn test_license_without_relationships() {
use crate::{KeygenRelationships, KeygenResponseData};
let license_data = KeygenResponseData {
id: "test-license-id".to_string(),
r#type: "licenses".to_string(),
attributes: LicenseAttributes {
key: "TEST-LICENSE-KEY".to_string(),
name: Some("Test License".to_string()),
expiry: None,
status: Some("active".to_string()),
uses: None,
max_machines: None,
max_cores: None,
max_uses: None,
max_processes: None,
max_users: None,
protected: None,
suspended: None,
permissions: None,
metadata: HashMap::new(),
},
relationships: KeygenRelationships {
policy: None,
account: None,
product: None,
group: None,
owner: None,
users: None,
machines: None,
environment: None,
license: None,
release: None,
other: HashMap::new(),
},
};
let license = License::from(license_data);
assert_eq!(license.policy, None);
assert_eq!(license.account_id, None);
assert_eq!(license.product_id, None);
assert_eq!(license.group_id, None);
assert_eq!(license.owner_id, None);
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_create_license_basic() {
let _m = mock("POST", "/v1/licenses")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "license-123",
"type": "licenses",
"attributes": {
"key": "LICENSE-KEY-123",
"name": "Test License",
"expiry": null,
"status": "active",
"uses": null,
"maxMachines": 5,
"maxCores": null,
"maxUses": null,
"maxProcesses": null,
"protected": null,
"suspended": false,
"metadata": {
"tier": "premium"
}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
},
"owner": {
"data": {
"type": "users",
"id": "user-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let mut metadata = HashMap::new();
metadata.insert("tier".to_string(), json!("premium"));
let request = LicenseCreateRequest::new("policy-123".to_string())
.with_name("Test License".to_string())
.with_max_machines(5)
.with_owner_id("user-123".to_string())
.with_metadata(metadata);
let result = License::create(request).await;
assert!(result.is_ok());
let license = result.unwrap();
assert_eq!(license.id, "license-123");
assert_eq!(license.key, "LICENSE-KEY-123");
assert_eq!(license.name, Some("Test License".to_string()));
assert_eq!(license.max_machines, Some(5));
assert_eq!(license.owner_id, Some("user-123".to_string()));
assert!(license.metadata.contains_key("tier"));
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_create_license_with_custom_key() {
let _m = mock("POST", "/v1/licenses")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "license-456",
"type": "licenses",
"attributes": {
"key": "CUSTOM-LICENSE-KEY",
"name": "Custom License",
"expiry": "2025-12-31T23:59:59Z",
"status": "active",
"uses": null,
"maxMachines": 10,
"maxCores": null,
"maxUses": null,
"maxProcesses": null,
"protected": null,
"suspended": false,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-456"
}
},
"group": {
"data": {
"type": "groups",
"id": "group-456"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let expiry = Utc.from_utc_datetime(
&chrono::NaiveDate::from_ymd_opt(2025, 12, 31)
.unwrap()
.and_hms_opt(23, 59, 59)
.unwrap(),
);
let request = LicenseCreateRequest::new("policy-456".to_string())
.with_name("Custom License".to_string())
.with_key("CUSTOM-LICENSE-KEY".to_string())
.with_expiry(expiry)
.with_max_machines(10)
.with_group_id("group-456".to_string());
let result = License::create(request).await;
assert!(result.is_ok());
let license = result.unwrap();
assert_eq!(license.id, "license-456");
assert_eq!(license.key, "CUSTOM-LICENSE-KEY");
assert_eq!(license.name, Some("Custom License".to_string()));
assert_eq!(license.max_machines, Some(10));
assert_eq!(license.group_id, Some("group-456".to_string()));
assert!(license.owner_id.is_none());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_create_license_minimal() {
let _m = mock("POST", "/v1/licenses")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "license-789",
"type": "licenses",
"attributes": {
"key": "AUTO-GENERATED-KEY",
"name": null,
"expiry": null,
"status": "active",
"uses": null,
"maxMachines": null,
"maxCores": null,
"maxUses": null,
"maxProcesses": null,
"protected": null,
"suspended": false,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-789"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let request = LicenseCreateRequest::new("policy-789".to_string());
let result = License::create(request).await;
assert!(result.is_ok());
let license = result.unwrap();
assert_eq!(license.id, "license-789");
assert_eq!(license.key, "AUTO-GENERATED-KEY");
assert!(license.name.is_none());
assert!(license.max_machines.is_none());
assert!(license.owner_id.is_none());
assert!(license.group_id.is_none());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_create_license_error() {
let _m = mock("POST", "/v1/licenses")
.with_status(422)
.with_header("content-type", "application/json")
.with_body(
json!({
"errors": [
{
"title": "Unprocessable Entity",
"detail": "Policy is required",
"code": "MISSING_POLICY"
}
]
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let request = LicenseCreateRequest::new("invalid-policy".to_string());
let result = License::create(request).await;
assert!(result.is_err());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_create_license_with_all_parameters() {
let _m = mock("POST", "/v1/licenses")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "license-comprehensive",
"type": "licenses",
"attributes": {
"key": "COMPREHENSIVE-LICENSE-KEY",
"name": "Comprehensive License",
"expiry": "2025-12-31T23:59:59Z",
"status": "active",
"uses": null,
"maxMachines": 10,
"maxProcesses": 5,
"maxUsers": 3,
"maxCores": 8,
"maxUses": 100,
"protected": true,
"suspended": false,
"permissions": ["activate", "deactivate", "read"],
"metadata": {
"tier": "enterprise",
"features": ["advanced", "premium"]
}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-comprehensive"
}
},
"owner": {
"data": {
"type": "users",
"id": "user-comprehensive"
}
},
"group": {
"data": {
"type": "groups",
"id": "group-comprehensive"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let expiry = Utc.from_utc_datetime(
&chrono::NaiveDate::from_ymd_opt(2025, 12, 31)
.unwrap()
.and_hms_opt(23, 59, 59)
.unwrap(),
);
let mut metadata = HashMap::new();
metadata.insert("tier".to_string(), json!("enterprise"));
metadata.insert("features".to_string(), json!(["advanced", "premium"]));
let request = LicenseCreateRequest::new("policy-comprehensive".to_string())
.with_name("Comprehensive License".to_string())
.with_key("COMPREHENSIVE-LICENSE-KEY".to_string())
.with_expiry(expiry)
.with_max_machines(10)
.with_max_processes(5)
.with_max_users(3)
.with_max_cores(8)
.with_max_uses(100)
.with_protected(true)
.with_suspended(false)
.with_permissions(vec![
"activate".to_string(),
"deactivate".to_string(),
"read".to_string(),
])
.with_metadata(metadata)
.with_owner_id("user-comprehensive".to_string())
.with_group_id("group-comprehensive".to_string());
let result = License::create(request).await;
assert!(result.is_ok());
let license = result.unwrap();
assert_eq!(license.id, "license-comprehensive");
assert_eq!(license.key, "COMPREHENSIVE-LICENSE-KEY");
assert_eq!(license.name, Some("Comprehensive License".to_string()));
assert_eq!(license.max_machines, Some(10));
assert_eq!(license.max_processes, Some(5));
assert_eq!(license.max_cores, Some(8));
assert_eq!(license.max_uses, Some(100));
assert!(license.protected == Some(true));
assert_eq!(license.suspended, Some(false));
assert_eq!(license.owner_id, Some("user-comprehensive".to_string()));
assert_eq!(license.group_id, Some("group-comprehensive".to_string()));
assert!(license.metadata.contains_key("tier"));
assert_eq!(
license.metadata.get("tier").unwrap().as_str().unwrap(),
"enterprise"
);
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_update_license_comprehensive() {
let license = create_test_license();
let _m = mock("PATCH", "/v1/licenses/test_license_id")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Updated License Name",
"expiry": "2025-12-31T23:59:59Z",
"status": "active",
"uses": null,
"maxMachines": 20,
"maxProcesses": 10,
"maxUsers": 5,
"maxCores": 16,
"maxUses": 200,
"protected": true,
"suspended": false,
"permissions": ["read", "write", "activate"],
"metadata": {
"tier": "enterprise",
"updated": true
}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let expiry = Utc.from_utc_datetime(
&chrono::NaiveDate::from_ymd_opt(2025, 12, 31)
.unwrap()
.and_hms_opt(23, 59, 59)
.unwrap(),
);
let mut metadata = HashMap::new();
metadata.insert("tier".to_string(), json!("enterprise"));
metadata.insert("updated".to_string(), json!(true));
let request = LicenseUpdateRequest::new()
.with_name("Updated License Name".to_string())
.with_expiry(expiry)
.with_max_machines(20)
.with_max_processes(10)
.with_max_users(5)
.with_max_cores(16)
.with_max_uses(200)
.with_protected(true)
.with_suspended(false)
.with_permissions(vec![
"read".to_string(),
"write".to_string(),
"activate".to_string(),
])
.with_metadata(metadata);
let result = license.update(request).await;
assert!(result.is_ok());
let updated_license = result.unwrap();
assert_eq!(updated_license.id, "test_license_id");
assert_eq!(
updated_license.name,
Some("Updated License Name".to_string())
);
assert_eq!(updated_license.max_machines, Some(20));
assert_eq!(updated_license.max_processes, Some(10));
assert_eq!(updated_license.max_users, Some(5));
assert_eq!(updated_license.max_cores, Some(16));
assert_eq!(updated_license.max_uses, Some(200));
assert!(updated_license.protected == Some(true));
assert_eq!(updated_license.suspended, Some(false));
assert!(updated_license.metadata.contains_key("tier"));
assert_eq!(
updated_license
.metadata
.get("tier")
.unwrap()
.as_str()
.unwrap(),
"enterprise"
);
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_update_license_clear_limits() {
let license = create_test_license();
let _m = mock("PATCH", "/v1/licenses/test_license_id")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Test License",
"expiry": null,
"status": "active",
"uses": null,
"maxMachines": null,
"maxProcesses": null,
"maxUsers": null,
"maxCores": null,
"maxUses": null,
"protected": null,
"suspended": false,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let request = LicenseUpdateRequest::new()
.clear_max_machines()
.clear_max_processes()
.clear_max_users()
.clear_max_cores()
.clear_max_uses();
let result = license.update(request).await;
assert!(result.is_ok());
let updated_license = result.unwrap();
assert_eq!(updated_license.max_machines, None);
assert_eq!(updated_license.max_processes, None);
assert_eq!(updated_license.max_users, None);
assert_eq!(updated_license.max_cores, None);
assert_eq!(updated_license.max_uses, None);
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_update_license_basic() {
let license = create_test_license();
let _m = mock("PATCH", "/v1/licenses/test_license_id")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Updated Name",
"expiry": "2025-06-30T23:59:59Z",
"status": "active",
"uses": null,
"maxMachines": null,
"maxCores": null,
"maxUses": null,
"maxProcesses": null,
"protected": null,
"suspended": false,
"metadata": {
"updated": true
}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let expiry = Utc.from_utc_datetime(
&chrono::NaiveDate::from_ymd_opt(2025, 6, 30)
.unwrap()
.and_hms_opt(23, 59, 59)
.unwrap(),
);
let mut metadata = HashMap::new();
metadata.insert("updated".to_string(), json!(true));
let request = LicenseUpdateRequest::new()
.with_name("Updated Name".to_string())
.with_expiry(expiry)
.with_metadata(metadata);
let result = license.update(request).await;
assert!(result.is_ok());
let updated_license = result.unwrap();
assert_eq!(updated_license.id, "test_license_id");
assert_eq!(updated_license.name, Some("Updated Name".to_string()));
assert!(updated_license.metadata.contains_key("updated"));
let _ = reset_config();
}
#[test]
fn test_license_update_request_builder() {
let mut metadata = HashMap::new();
metadata.insert("tier".to_string(), json!("premium"));
let request = LicenseUpdateRequest::new()
.with_name("Test License".to_string())
.with_max_machines(10)
.with_protected(true)
.with_metadata(metadata.clone());
assert_eq!(request.name, Some("Test License".to_string()));
assert!(matches!(request.max_machines, UpdateField::Set(10)));
assert!(request.protected == Some(true));
assert_eq!(request.metadata, Some(metadata));
let request_with_clear = LicenseUpdateRequest::new()
.with_max_machines(5)
.clear_max_machines();
assert!(matches!(
request_with_clear.max_machines,
UpdateField::Clear
));
let body = request.to_json_body();
let data = body.get("data").unwrap();
let attributes = data.get("attributes").unwrap();
assert_eq!(
attributes.get("name").unwrap().as_str().unwrap(),
"Test License"
);
assert_eq!(attributes.get("maxMachines").unwrap().as_i64().unwrap(), 10);
assert!(attributes.get("protected").unwrap().as_bool().unwrap());
assert!(attributes.get("metadata").is_some());
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_license_list_pagination_with_page_number() {
let _m = mock("GET", "/v1/licenses")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page[number]".into(), "2".into()),
mockito::Matcher::UrlEncoded("page[size]".into(), "15".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": [
{
"id": "license-1",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-1",
"name": "Test License 1",
"expiry": null,
"status": "active",
"uses": null,
"maxMachines": null,
"maxCores": null,
"maxUses": null,
"maxProcesses": null,
"protected": null,
"suspended": false,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
]
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let options = LicenseListOptions {
page_number: Some(2),
page_size: Some(15),
..Default::default()
};
let result = License::list(Some(&options)).await;
assert!(result.is_ok());
let licenses = result.unwrap();
assert_eq!(licenses.len(), 1);
assert_eq!(licenses[0].id, "license-1");
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_license_list_pagination_with_limit_only() {
let _m = mock("GET", "/v1/licenses")
.match_query(mockito::Matcher::UrlEncoded("limit".into(), "5".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": []
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let options = LicenseListOptions {
limit: Some(5),
..Default::default()
};
let result = License::list(Some(&options)).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[tokio::test]
async fn test_pagination_options_with_new_parameters() {
let _m = mock("GET", "/v1/licenses/test_license_id/machines")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page[number]".into(), "3".into()),
mockito::Matcher::UrlEncoded("page[size]".into(), "25".into()),
mockito::Matcher::UrlEncoded("limit".into(), "50".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": []
})
.to_string(),
)
.create();
let config = KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
..Default::default()
};
let _ = set_config(config.clone());
let license = create_test_license().with_config(config);
let pagination_options = PaginationOptions {
limit: Some(50),
page_number: Some(3),
page_size: Some(25),
};
let result = license.machines(Some(&pagination_options)).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_attach_entitlements() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/entitlements")
.with_status(204)
.with_header("content-type", "application/json")
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let entitlement_ids = vec!["entitlement-1".to_string(), "entitlement-2".to_string()];
let result = license.attach_entitlements(&entitlement_ids).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_detach_entitlements() {
let license = create_test_license();
let _m = mock("DELETE", "/v1/licenses/test_license_id/entitlements")
.with_status(204)
.with_header("content-type", "application/json")
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let entitlement_ids = vec!["entitlement-1".to_string(), "entitlement-2".to_string()];
let result = license.detach_entitlements(&entitlement_ids).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_attach_entitlements_empty_list() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/entitlements")
.with_status(204)
.with_header("content-type", "application/json")
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let entitlement_ids: Vec<String> = vec![];
let result = license.attach_entitlements(&entitlement_ids).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_detach_entitlements_empty_list() {
let license = create_test_license();
let _m = mock("DELETE", "/v1/licenses/test_license_id/entitlements")
.with_status(204)
.with_header("content-type", "application/json")
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let entitlement_ids: Vec<String> = vec![];
let result = license.detach_entitlements(&entitlement_ids).await;
assert!(result.is_ok());
let _ = reset_config();
}
#[tokio::test]
async fn test_increment_usage() {
let license = create_test_license();
let _m = mock(
"POST",
"/v1/licenses/test_license_id/actions/increment-usage",
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Test License",
"expiry": null,
"status": "active",
"uses": 6, "maxMachines": null,
"maxCores": null,
"maxUses": 100,
"maxProcesses": null,
"maxUsers": null,
"protected": null,
"suspended": false,
"permissions": null,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
product: "test_product".to_string(),
license_key: Some("TEST-LICENSE-KEY".to_string()),
..Default::default()
});
let result = license.increment_usage().await;
assert!(result.is_ok());
let updated_license = result.unwrap();
assert_eq!(updated_license.uses, Some(6));
assert_eq!(updated_license.id, "test_license_id");
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_decrement_usage() {
let license = create_test_license();
let _m = mock(
"POST",
"/v1/licenses/test_license_id/actions/decrement-usage",
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Test License",
"expiry": null,
"status": "active",
"uses": 4, "maxMachines": null,
"maxCores": null,
"maxUses": 100,
"maxProcesses": null,
"maxUsers": null,
"protected": null,
"suspended": false,
"permissions": null,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let result = license.decrement_usage().await;
assert!(result.is_ok());
let updated_license = result.unwrap();
assert_eq!(updated_license.uses, Some(4));
assert_eq!(updated_license.id, "test_license_id");
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_reset_usage() {
let license = create_test_license();
let _m = mock("POST", "/v1/licenses/test_license_id/actions/reset-usage")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "test_license_id",
"type": "licenses",
"attributes": {
"key": "TEST-LICENSE-KEY",
"name": "Test License",
"expiry": null,
"status": "active",
"uses": 0, "maxMachines": null,
"maxCores": null,
"maxUses": 100,
"maxProcesses": null,
"maxUsers": null,
"protected": null,
"suspended": false,
"permissions": null,
"metadata": {}
},
"relationships": {
"policy": {
"data": {
"type": "policies",
"id": "policy-123"
}
}
}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let result = license.reset_usage().await;
assert!(result.is_ok());
let updated_license = result.unwrap();
assert_eq!(updated_license.uses, Some(0));
assert_eq!(updated_license.id, "test_license_id");
let _ = reset_config();
}
#[test]
fn test_scheme_code_serialization() {
assert_eq!(
serde_json::to_string(&SchemeCode::Ed25519Sign).unwrap(),
"\"ED25519_SIGN\""
);
assert_eq!(
serde_json::to_string(&SchemeCode::EcdsaP256Sign).unwrap(),
"\"ECDSA_P256_SIGN\""
);
assert_eq!(
serde_json::to_string(&SchemeCode::Rsa2048Pkcs1PssSignV2).unwrap(),
"\"RSA_2048_PKCS1_PSS_SIGN_V2\""
);
assert_eq!(
serde_json::to_string(&SchemeCode::LegacyEncrypt).unwrap(),
"\"LEGACY_ENCRYPT\""
);
}
#[test]
fn test_scheme_code_deserialization() {
assert_eq!(
serde_json::from_str::<SchemeCode>("\"ED25519_SIGN\"").unwrap(),
SchemeCode::Ed25519Sign
);
assert_eq!(
serde_json::from_str::<SchemeCode>("\"ECDSA_P256_SIGN\"").unwrap(),
SchemeCode::EcdsaP256Sign
);
assert_eq!(
serde_json::from_str::<SchemeCode>("\"RSA_2048_PKCS1_PSS_SIGN_V2\"").unwrap(),
SchemeCode::Rsa2048Pkcs1PssSignV2
);
assert_eq!(
serde_json::from_str::<SchemeCode>("\"LEGACY_ENCRYPT\"").unwrap(),
SchemeCode::LegacyEncrypt
);
}
}