use licenseseat::{Config, LicenseSeat};
use serde_json::json;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use wiremock::matchers::{header, method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn test_config(base_url: &str) -> Config {
let unique_prefix = format!(
"api_test_{}_{}_{}_",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos(),
TEST_COUNTER.fetch_add(1, Ordering::SeqCst)
);
Config {
api_key: "test-api-key".into(),
product_slug: "test-product".into(),
api_base_url: base_url.into(),
storage_prefix: unique_prefix,
auto_validate_interval: Duration::from_secs(0),
heartbeat_interval: Duration::from_secs(0),
debug: true,
..Default::default()
}
}
fn activation_response() -> serde_json::Value {
json!({
"object": "activation",
"id": "act-12345-uuid",
"device_id": "device-123",
"device_name": "Test Device",
"license_key": "TEST-KEY",
"activated_at": "2025-01-01T00:00:00Z",
"deactivated_at": null,
"ip_address": "127.0.0.1",
"metadata": null,
"license": {
"object": "license",
"key": "TEST-KEY",
"status": "active",
"starts_at": null,
"expires_at": null,
"mode": "hardware_locked",
"plan_key": "pro",
"seat_limit": 5,
"active_seats": 1,
"active_entitlements": [],
"metadata": null,
"product": {
"slug": "test-product",
"name": "Test App"
}
}
})
}
#[tokio::test]
async fn test_retry_on_5xx_then_success() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(ResponseTemplate::new(502).set_body_string(r#"{"error":"bad gateway"}"#))
.up_to_n_times(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_retry_on_503_service_unavailable() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(
ResponseTemplate::new(503).set_body_string(r#"{"error":"service unavailable"}"#),
)
.up_to_n_times(2)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_4xx_client_errors_return_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
"error": {
"code": "invalid_request",
"message": "Bad request"
}
})))
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_404_returns_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"error": {
"code": "license_not_found",
"message": "License not found"
}
})))
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_activate_encodes_license_key_in_request_path() {
let server = MockServer::start().await;
let license_key = "TEST KEY/with#chars";
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate(license_key).await;
assert!(result.is_ok());
let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 1);
assert_eq!(
requests[0].url.path(),
"/products/test-product/licenses/TEST%20KEY%2Fwith%23chars/activate"
);
}
#[tokio::test]
async fn test_activate_preserves_api_base_url_prefix_without_trailing_slash() {
let server = MockServer::start().await;
let base_url = format!("{}/api/v1", server.uri());
Mock::given(method("POST"))
.and(path(
"/api/v1/products/test-product/licenses/TEST-KEY/activate",
))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&base_url));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_activate_preserves_api_base_url_prefix_with_trailing_slash() {
let server = MockServer::start().await;
let base_url = format!("{}/api/v1/", server.uri());
Mock::given(method("POST"))
.and(path(
"/api/v1/products/test-product/licenses/TEST-KEY/activate",
))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&base_url));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_health_preserves_api_base_url_prefix() {
let server = MockServer::start().await;
let base_url = format!("{}/api/v1", server.uri());
Mock::given(method("GET"))
.and(path("/api/v1/health"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({
"object": "health",
"status": "healthy",
"api_version": "v1",
"timestamp": "2026-03-31T04:00:00Z"
}))
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&base_url));
let result = sdk.health().await;
assert!(matches!(result, Ok(true)));
}
#[tokio::test]
async fn test_401_unauthorized_returns_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": {
"code": "invalid_api_key",
"message": "Invalid API key"
}
})))
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_authorization_header() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.and(header("Authorization", "Bearer test-api-key"))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_content_type_header() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.and(header("Content-Type", "application/json"))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_user_agent_header_present() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(
ResponseTemplate::new(201)
.set_body_json(activation_response())
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_api_error_parsing() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"error": {
"code": "license_not_found",
"message": "The license key was not found"
}
})))
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("INVALID-KEY").await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = format!("{:?}", err);
assert!(err_str.contains("404") || err_str.contains("license_not_found"));
}
#[tokio::test]
async fn test_api_error_without_code() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"/products/.*/licenses/.*/activate"))
.respond_with(ResponseTemplate::new(500).set_body_json(json!({
"error": {
"message": "Internal server error"
}
})))
.mount(&server)
.await;
let sdk = LicenseSeat::new(test_config(&server.uri()));
let result = sdk.activate("TEST-KEY").await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = format!("{:?}", err);
assert!(err_str.contains("500") || err_str.contains("error") || err_str.contains("Internal"));
}