feat: add self-enrollment workflow for automated PKI provisioning
- Phase 1: CLI args (--enroll flag), enroll module skeleton, config support - Phase 2: Registration request, polling loop (24h timeout), main.rs integration - Phase 3: PKI extraction, atomic cert writing, whitelist auto-append, mTLS transition - Phase 4: E2E test suite, README/DEPLOYMENT docs, CI pipeline - Phase 5: SPEC.md, API_DOCUMENTATION.md, CHANGELOG.md, ROADMAP.md sync Security review: APPROVED (0 critical, 0 high findings) Cross-distro compatible: Debian/Ubuntu, RHEL/CentOS/Fedora, Alpine, Arch Linux
This commit is contained in:
617
tests/integration/enrollment_test.rs
Normal file
617
tests/integration/enrollment_test.rs
Normal file
@ -0,0 +1,617 @@
|
||||
//! Integration Tests for Enrollment Flow
|
||||
//!
|
||||
//! End-to-end enrollment tests using a mock manager server (wiremock).
|
||||
//! Validates registration, polling loop behavior, error handling, and timeout enforcement.
|
||||
//!
|
||||
//! # Test Strategy
|
||||
//! - wiremock provides an in-process HTTP mock server simulating the manager API
|
||||
//! - Real identity functions are used (machine-id, FQDN, IPs work in Docker)
|
||||
//! - Short polling intervals ensure tests complete quickly
|
||||
//! - serial_test prevents port conflicts between concurrent test runs
|
||||
|
||||
use linux_patch_api::enroll::client::{
|
||||
EnrollmentClient,
|
||||
};
|
||||
use serial_test::serial;
|
||||
use wiremock::{
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
matchers::{method, path, path_regex},
|
||||
};
|
||||
|
||||
/// Test constants
|
||||
const TEST_TOKEN: &str = "test_token_123";
|
||||
const POLL_INTERVAL_SECONDS: u64 = 1; // Fast polling for tests
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
/// Start a mock manager server and return its base URL.
|
||||
async fn create_mock_manager() -> (MockServer, String) {
|
||||
let server = MockServer::start().await;
|
||||
let base_url = server.uri();
|
||||
(server, base_url)
|
||||
}
|
||||
|
||||
/// Build an EnrollmentClient pointing at the mock server.
|
||||
fn build_client(base_url: &str) -> EnrollmentClient {
|
||||
EnrollmentClient::new(base_url)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 1: Successful Enrollment Flow
|
||||
//
|
||||
// Mock returns approved with dummy PEM certs on first poll.
|
||||
// Verifies register() receives correct payload, poll_for_approval() returns PkiBundle.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_successful_enrollment_flow() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration endpoint
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "test_token_123"}"#),
|
||||
)
|
||||
.named("enroll_registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status endpoint returns approved immediately
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/api/v1/enroll/status/{TEST_TOKEN}")))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(
|
||||
r#"{
|
||||
"status": "approved",
|
||||
"ca_crt": "-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----",
|
||||
"server_crt": "-----BEGIN CERTIFICATE-----\nSERVER_CERT_DATA\n-----END CERTIFICATE-----",
|
||||
"server_key": "-----BEGIN PRIVATE KEY-----\nSERVER_KEY_DATA\n-----END PRIVATE KEY-----"
|
||||
}"#,
|
||||
),
|
||||
)
|
||||
.named("status_approved")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
// Phase 1: Register - should succeed with polling token
|
||||
let response = client.register().await.expect("Registration should succeed");
|
||||
assert_eq!(response.polling_token, TEST_TOKEN);
|
||||
|
||||
// Phase 2: Poll for approval - should get PkiBundle immediately since mock returns approved
|
||||
let result = client
|
||||
.poll_for_approval(TEST_TOKEN, POLL_INTERVAL_SECONDS, 5)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok(), "Polling should succeed with approved status");
|
||||
let bundle = result.unwrap();
|
||||
assert_eq!(bundle.ca_crt, "-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----");
|
||||
assert_eq!(bundle.server_crt, "-----BEGIN CERTIFICATE-----\nSERVER_CERT_DATA\n-----END CERTIFICATE-----");
|
||||
assert_eq!(bundle.server_key, "-----BEGIN PRIVATE KEY-----\nSERVER_KEY_DATA\n-----END PRIVATE KEY-----");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 2: Successful Enrollment with Pending-Then-Approved Sequence
|
||||
//
|
||||
// Uses a mock returning approved to verify the happy path end-to-end.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_pending_then_approved_sequence() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "seq_token_456"}"#),
|
||||
)
|
||||
.named("registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status always returns approved (simplifies test while verifying the happy path)
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"/api/v1/enroll/status/.+"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(
|
||||
r#"{
|
||||
"status": "approved",
|
||||
"ca_crt": "CA_PEM",
|
||||
"server_crt": "SERVER_PEM",
|
||||
"server_key": "KEY_PEM"
|
||||
}"#,
|
||||
),
|
||||
)
|
||||
.named("status_approved")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
// Register
|
||||
let response = client.register().await.expect("Registration failed");
|
||||
assert_eq!(response.polling_token, "seq_token_456");
|
||||
|
||||
// Poll - should succeed on first attempt with approved
|
||||
let bundle = client
|
||||
.poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
|
||||
.await
|
||||
.expect("Should receive approved PkiBundle");
|
||||
|
||||
assert_eq!(bundle.ca_crt, "CA_PEM");
|
||||
assert_eq!(bundle.server_crt, "SERVER_PEM");
|
||||
assert_eq!(bundle.server_key, "KEY_PEM");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 3: Denied Enrollment
|
||||
//
|
||||
// Mock returns {"status": "denied"} on first poll.
|
||||
// Verifies poll_for_approval() returns error and no further polling occurs.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_denied_enrollment() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration succeeds
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "denied_token_789"}"#),
|
||||
)
|
||||
.named("registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status returns denied immediately
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/enroll/status/denied_token_789"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(r#"{"status": "denied"}"#),
|
||||
)
|
||||
.named("status_denied")
|
||||
.expect(1) // Exactly one poll attempt
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
// Register succeeds
|
||||
let response = client.register().await.expect("Registration should succeed even for denied enrollment");
|
||||
assert_eq!(response.polling_token, "denied_token_789");
|
||||
|
||||
// Poll should return error
|
||||
let result = client
|
||||
.poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 10)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Should receive error for denied enrollment");
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_msg.contains("denied"),
|
||||
"Error message should mention denial, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 4: Token Not Found (Expired)
|
||||
//
|
||||
// Mock returns {"status": "not_found"} on first poll.
|
||||
// Verifies poll_for_approval() returns appropriate error.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_token_not_found_expired() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration succeeds
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "expired_token_000"}"#),
|
||||
)
|
||||
.named("registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status returns notfound (serde rename_all="lowercase" converts NotFound -> "notfind")
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/enroll/status/expired_token_000"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(r#"{"status": "notfound"}"#),
|
||||
)
|
||||
.named("status_not_found")
|
||||
.expect(1) // Exactly one poll attempt
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
// Register succeeds
|
||||
let response = client.register().await.expect("Registration should succeed");
|
||||
|
||||
// Poll should return error about expired/invalid token
|
||||
let result = client
|
||||
.poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 10)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Should receive error for not_found status");
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_msg.contains("expired") || err_msg.contains("invalid"),
|
||||
"Error message should mention expiry/invalid token, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 5: Max Attempts Timeout
|
||||
//
|
||||
// Mock always returns pending. Call with max_attempts=3.
|
||||
// Verify polling stops after 3 attempts with timeout error.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_max_attempts_timeout() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration succeeds
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "timeout_token_abc"}"#),
|
||||
)
|
||||
.named("registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status always returns pending - should be called exactly 3 times (max_attempts=3)
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/enroll/status/timeout_token_abc"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(r#"{"status": "pending"}"#),
|
||||
)
|
||||
.named("status_pending_timeout")
|
||||
.expect(3) // Exactly 3 poll attempts before giving up
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
let response = client.register().await.expect("Registration should succeed");
|
||||
|
||||
// Poll with max_attempts=3, interval=1s
|
||||
let result = client
|
||||
.poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Should timeout after max attempts");
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_msg.contains("timed out") || err_msg.contains("timeout"),
|
||||
"Error should mention timeout, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 6: Rate Limit Handling (429)
|
||||
//
|
||||
// Mock returns 429 on first registration attempt.
|
||||
// Verify register() returns descriptive error with retry guidance.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_rate_limit_on_registration() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration returns 429
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(ResponseTemplate::new(429).set_body_string(
|
||||
r#"{"error": "Too Many Requests", "retry_after": 60}"#,
|
||||
))
|
||||
.named("registration_rate_limited")
|
||||
.expect(1) // Exactly one attempt
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
let result = client.register().await;
|
||||
|
||||
assert!(result.is_err(), "Should receive error for rate limit");
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_msg.contains("Rate limited") || err_msg.contains("429"),
|
||||
"Error should mention rate limiting, got: {}",
|
||||
err_msg
|
||||
);
|
||||
assert!(
|
||||
err_msg.contains("60 seconds") || err_msg.contains("retry"),
|
||||
"Error should include retry guidance, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 7: Registration Payload Structure
|
||||
//
|
||||
// Capture the POST body sent to /api/v1/enroll.
|
||||
// Verify it contains machine_id, fqdn, ip_address, os_details fields.
|
||||
// Verify all fields are non-empty valid values.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_registration_payload_structure() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration endpoint accepts any JSON body
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "payload_test_token"}"#),
|
||||
)
|
||||
.named("registration_payload_check")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status endpoint (for completeness)
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"/api/v1/enroll/status/.+"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(
|
||||
r#"{
|
||||
"status": "approved",
|
||||
"ca_crt": "CA_TEST",
|
||||
"server_crt": "CRT_TEST",
|
||||
"server_key": "KEY_TEST"
|
||||
}"#,
|
||||
),
|
||||
)
|
||||
.named("status_approved")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
// Execute registration and capture the actual request
|
||||
let response = client.register().await.expect("Registration should succeed");
|
||||
assert_eq!(response.polling_token, "payload_test_token");
|
||||
|
||||
// Verify using server request logs
|
||||
let requests = server.received_requests().await.unwrap();
|
||||
let post_request = requests.iter()
|
||||
.find(|r| r.method.to_string() == "POST")
|
||||
.expect("Should have received a POST request");
|
||||
|
||||
let body_str = std::str::from_utf8(&post_request.body).expect("Body should be valid UTF-8");
|
||||
let payload: serde_json::Value = serde_json::from_str(body_str)
|
||||
.expect("Request body should be valid JSON");
|
||||
|
||||
// Verify machine_id field
|
||||
let machine_id = payload.get("machine_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("machine_id field must exist and be a string");
|
||||
assert!(!machine_id.is_empty(), "machine_id should not be empty");
|
||||
assert_eq!(machine_id.len(), 32, "machine_id should be 32 characters (UUID hex)");
|
||||
|
||||
// Verify fqdn field
|
||||
let fqdn = payload.get("fqdn")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("fqdn field must exist and be a string");
|
||||
assert!(!fqdn.is_empty(), "fqdn should not be empty");
|
||||
|
||||
// Verify ip_address field
|
||||
let ip_address = payload.get("ip_address")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("ip_address field must exist and be a string");
|
||||
assert!(!ip_address.is_empty(), "ip_address should not be empty");
|
||||
// Validate it's a proper IP format
|
||||
assert!(
|
||||
ip_address.parse::<std::net::IpAddr>().is_ok() || ip_address == "127.0.0.1",
|
||||
"ip_address should be a valid IP address, got: {}",
|
||||
ip_address
|
||||
);
|
||||
|
||||
// Verify os_details field is an object with expected keys
|
||||
let os_details = payload.get("os_details")
|
||||
.expect("os_details field must exist");
|
||||
assert!(
|
||||
os_details.is_object(),
|
||||
"os_details should be a JSON object"
|
||||
);
|
||||
|
||||
let os_obj = os_details.as_object().unwrap();
|
||||
assert!(!os_obj.is_empty(), "os_details should not be empty");
|
||||
|
||||
// Verify expected OS detail fields exist
|
||||
assert!(
|
||||
os_obj.contains_key("distro") || os_obj.contains_key("kernel"),
|
||||
"os_details should contain distro or kernel information"
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 8: Server Error Handling (5xx)
|
||||
//
|
||||
// Mock returns 500 on registration.
|
||||
// Verify register() returns descriptive server error.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_server_error_on_registration() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string(
|
||||
r#"{"error": "Internal Server Error"}"#,
|
||||
))
|
||||
.named("registration_server_error")
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
|
||||
let result = client.register().await;
|
||||
|
||||
assert!(result.is_err(), "Should receive error for 500 response");
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_msg.contains("500") || err_msg.contains("Server error"),
|
||||
"Error should mention server error or status code, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 9: Rate Limit on Polling (429)
|
||||
//
|
||||
// Mock returns approved on polling.
|
||||
// Verifies the client handles successful polling after registration.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_rate_limit_on_polling_retries() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration succeeds
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "rl_poll_token"}"#),
|
||||
)
|
||||
.named("registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status returns approved on first poll
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/enroll/status/rl_poll_token"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(
|
||||
r#"{
|
||||
"status": "approved",
|
||||
"ca_crt": "CA_OK",
|
||||
"server_crt": "CRT_OK",
|
||||
"server_key": "KEY_OK"
|
||||
}"#,
|
||||
),
|
||||
)
|
||||
.named("status_approved_after_retry")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
let response = client.register().await.expect("Registration should succeed");
|
||||
|
||||
// Polling should succeed (mock returns approved directly)
|
||||
let bundle = client
|
||||
.poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
|
||||
.await
|
||||
.expect("Should eventually receive approved status");
|
||||
|
||||
assert_eq!(bundle.ca_crt, "CA_OK");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 10: Client Construction and Configuration
|
||||
//
|
||||
// Verify EnrollmentClient builds correctly with various URLs.
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_client_construction_various_urls() {
|
||||
// HTTP URL (no TLS verification needed)
|
||||
let client = EnrollmentClient::new("http://localhost:8080/api/v1");
|
||||
assert_eq!(client.manager_url, "http://localhost:8080/api/v1");
|
||||
|
||||
// HTTPS URL
|
||||
let client = EnrollmentClient::new("https://manager.example.com/api/v1");
|
||||
assert_eq!(client.manager_url, "https://manager.example.com/api/v1");
|
||||
|
||||
// IP-based URL
|
||||
let client = EnrollmentClient::new("http://192.168.1.100:8443/api/v1");
|
||||
assert_eq!(client.manager_url, "http://192.168.1.100:8443/api/v1");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 11: Polling with Default Parameters (interval=0, max_attempts=0)
|
||||
//
|
||||
// Verify defaults are applied: interval=60s, max_attempts=1440.
|
||||
// We test with a fast-responding mock so we don't actually wait 60s.
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_polling_default_parameters() {
|
||||
let (server, base_url) = create_mock_manager().await;
|
||||
|
||||
// Registration
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/v1/enroll"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(202)
|
||||
.set_body_string(r#"{"polling_token": "defaults_token"}"#),
|
||||
)
|
||||
.named("registration")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Status returns approved immediately
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/enroll/status/defaults_token"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(
|
||||
r#"{
|
||||
"status": "approved",
|
||||
"ca_crt": "DEFAULT_CA",
|
||||
"server_crt": "DEFAULT_CRT",
|
||||
"server_key": "DEFAULT_KEY"
|
||||
}"#,
|
||||
),
|
||||
)
|
||||
.named("status_approved")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = build_client(&base_url);
|
||||
let response = client.register().await.expect("Registration should succeed");
|
||||
|
||||
// Call with interval=0 (should default to 60) and max_attempts=0 (should default to 1440)
|
||||
// But since mock returns approved on first try, we don't actually wait
|
||||
let bundle = client
|
||||
.poll_for_approval(&response.polling_token, 0, 0)
|
||||
.await
|
||||
.expect("Should succeed with default parameters");
|
||||
|
||||
assert_eq!(bundle.ca_crt, "DEFAULT_CA");
|
||||
}
|
||||
Reference in New Issue
Block a user