use mcias_client::{Client, ClientOptions, MciasError}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; async fn admin_client(server: &MockServer) -> Client { Client::new(&server.uri(), ClientOptions { token: Some("admin-token".to_string()), ..Default::default() }) .unwrap() } fn json_body(body: serde_json::Value) -> ResponseTemplate { ResponseTemplate::new(200) .set_body_json(body) .insert_header("content-type", "application/json") } fn json_body_status(status: u16, body: serde_json::Value) -> ResponseTemplate { ResponseTemplate::new(status) .set_body_json(body) .insert_header("content-type", "application/json") } // ---- health ---- #[tokio::test] async fn test_health_ok() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/health")) .respond_with(json_body(serde_json::json!({"status": "ok"}))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); c.health().await.expect("health should succeed"); } #[tokio::test] async fn test_health_server_error() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/health")) .respond_with(json_body_status(500, serde_json::json!({"error": "oops"}))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let err = c.health().await.unwrap_err(); assert!(matches!(err, MciasError::Server { .. }), "expected Server error, got {err:?}"); } // ---- public key ---- #[tokio::test] async fn test_get_public_key() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/keys/public")) .respond_with(json_body(serde_json::json!({ "kty": "OKP", "crv": "Ed25519", "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" }))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let pk = c.get_public_key().await.expect("get_public_key should succeed"); assert_eq!(pk.kty, "OKP"); assert_eq!(pk.crv, "Ed25519"); } // ---- login ---- #[tokio::test] async fn test_login_success() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/auth/login")) .respond_with(json_body(serde_json::json!({ "token": "jwt-token", "expires_at": "2099-01-01T00:00:00Z" }))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let (tok, exp) = c.login("alice", "s3cret", None).await.unwrap(); assert_eq!(tok, "jwt-token"); assert_eq!(exp, "2099-01-01T00:00:00Z"); // Token stored in client. assert_eq!(c.token().await.as_deref(), Some("jwt-token")); } #[tokio::test] async fn test_login_bad_credentials() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/auth/login")) .respond_with(json_body_status(401, serde_json::json!({"error": "invalid credentials"}))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let err = c.login("alice", "wrong", None).await.unwrap_err(); assert!(matches!(err, MciasError::Auth(_)), "expected Auth error, got {err:?}"); } // ---- logout ---- #[tokio::test] async fn test_logout_clears_token() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/auth/logout")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions { token: Some("existing-token".to_string()), ..Default::default() }) .unwrap(); c.logout().await.unwrap(); assert!(c.token().await.is_none(), "token should be cleared after logout"); } // ---- renew ---- #[tokio::test] async fn test_renew_token() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/auth/renew")) .respond_with(json_body(serde_json::json!({ "token": "new-token", "expires_at": "2099-06-01T00:00:00Z" }))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions { token: Some("old-token".to_string()), ..Default::default() }) .unwrap(); let (tok, _) = c.renew_token().await.unwrap(); assert_eq!(tok, "new-token"); assert_eq!(c.token().await.as_deref(), Some("new-token")); } // ---- validate token ---- #[tokio::test] async fn test_validate_token_valid() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/token/validate")) .respond_with(json_body(serde_json::json!({ "valid": true, "sub": "uuid-123", "roles": ["admin"], "expires_at": "2099-01-01T00:00:00Z" }))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let claims = c.validate_token("good-token").await.unwrap(); assert!(claims.valid); assert_eq!(claims.sub, "uuid-123"); assert_eq!(claims.roles, vec!["admin"]); } #[tokio::test] async fn test_validate_token_invalid() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/token/validate")) .respond_with(json_body(serde_json::json!({"valid": false}))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let claims = c.validate_token("garbage").await.unwrap(); assert!(!claims.valid, "expected valid=false"); } // ---- accounts ---- #[tokio::test] async fn test_create_account() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/accounts")) .respond_with( ResponseTemplate::new(201) .set_body_json(serde_json::json!({ "id": "new-uuid", "username": "bob", "account_type": "human", "status": "active", "created_at": "2023-11-15T12:00:00Z", "updated_at": "2023-11-15T12:00:00Z", "totp_enabled": false })) .insert_header("content-type", "application/json"), ) .mount(&server) .await; let c = admin_client(&server).await; let a = c.create_account("bob", Some("pass123"), "human").await.unwrap(); assert_eq!(a.username, "bob"); assert_eq!(a.account_type, "human"); assert_eq!(a.status, "active"); } #[tokio::test] async fn test_create_account_conflict() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/accounts")) .respond_with(json_body_status(409, serde_json::json!({"error": "username already exists"}))) .mount(&server) .await; let c = admin_client(&server).await; let err = c.create_account("dup", Some("pass"), "human").await.unwrap_err(); assert!(matches!(err, MciasError::Conflict(_)), "expected Conflict error, got {err:?}"); } #[tokio::test] async fn test_list_accounts() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/accounts")) .respond_with(json_body(serde_json::json!([ { "id": "uuid-1", "username": "alice", "account_type": "human", "status": "active", "created_at": "2023-11-15T12:00:00Z", "updated_at": "2023-11-15T12:00:00Z", "totp_enabled": false } ]))) .mount(&server) .await; let c = admin_client(&server).await; let accounts = c.list_accounts().await.unwrap(); assert_eq!(accounts.len(), 1); assert_eq!(accounts[0].username, "alice"); } #[tokio::test] async fn test_get_account_not_found() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/accounts/missing")) .respond_with(json_body_status(404, serde_json::json!({"error": "account not found"}))) .mount(&server) .await; let c = admin_client(&server).await; let err = c.get_account("missing").await.unwrap_err(); assert!(matches!(err, MciasError::NotFound(_)), "expected NotFound, got {err:?}"); } #[tokio::test] async fn test_update_account() { let server = MockServer::start().await; Mock::given(method("PATCH")) .and(path("/v1/accounts/uuid-1")) .respond_with(json_body(serde_json::json!({ "id": "uuid-1", "username": "alice", "account_type": "human", "status": "inactive", "created_at": "2023-11-15T12:00:00Z", "updated_at": "2023-11-15T13:00:00Z", "totp_enabled": false }))) .mount(&server) .await; let c = admin_client(&server).await; let a = c.update_account("uuid-1", "inactive").await.unwrap(); assert_eq!(a.status, "inactive"); } #[tokio::test] async fn test_delete_account() { let server = MockServer::start().await; Mock::given(method("DELETE")) .and(path("/v1/accounts/uuid-1")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; let c = admin_client(&server).await; c.delete_account("uuid-1").await.unwrap(); } // ---- roles ---- #[tokio::test] async fn test_get_set_roles() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/accounts/uuid-1/roles")) .respond_with(json_body(serde_json::json!(["admin", "viewer"]))) .mount(&server) .await; Mock::given(method("PUT")) .and(path("/v1/accounts/uuid-1/roles")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; let c = admin_client(&server).await; let roles = c.get_roles("uuid-1").await.unwrap(); assert_eq!(roles, vec!["admin", "viewer"]); c.set_roles("uuid-1", &["editor"]).await.unwrap(); } // ---- tokens ---- #[tokio::test] async fn test_issue_service_token() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/v1/token/issue")) .respond_with(json_body(serde_json::json!({ "token": "svc-token", "expires_at": "2099-01-01T00:00:00Z" }))) .mount(&server) .await; let c = admin_client(&server).await; let (tok, _) = c.issue_service_token("svc-uuid").await.unwrap(); assert_eq!(tok, "svc-token"); } #[tokio::test] async fn test_revoke_token() { let server = MockServer::start().await; Mock::given(method("DELETE")) .and(path("/v1/token/some-jti")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; let c = admin_client(&server).await; c.revoke_token("some-jti").await.unwrap(); } // ---- pg creds ---- #[tokio::test] async fn test_pg_creds_not_found() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/accounts/uuid-1/pgcreds")) .respond_with(json_body_status(404, serde_json::json!({"error": "no pg credentials found"}))) .mount(&server) .await; let c = admin_client(&server).await; let err = c.get_pg_creds("uuid-1").await.unwrap_err(); assert!(matches!(err, MciasError::NotFound(_))); } #[tokio::test] async fn test_set_get_pg_creds() { let server = MockServer::start().await; Mock::given(method("PUT")) .and(path("/v1/accounts/uuid-1/pgcreds")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/v1/accounts/uuid-1/pgcreds")) .respond_with(json_body(serde_json::json!({ "host": "db.example.com", "port": 5432, "database": "mydb", "username": "dbuser", "password": "dbpass" }))) .mount(&server) .await; let c = admin_client(&server).await; c.set_pg_creds("uuid-1", "db.example.com", 5432, "mydb", "dbuser", "dbpass") .await .unwrap(); let creds = c.get_pg_creds("uuid-1").await.unwrap(); assert_eq!(creds.host, "db.example.com"); assert_eq!(creds.port, 5432); assert_eq!(creds.password, "dbpass"); } // ---- error type coverage ---- #[tokio::test] async fn test_forbidden_error() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v1/accounts")) .respond_with(json_body_status(403, serde_json::json!({"error": "forbidden"}))) .mount(&server) .await; // Use a non-admin token. let c = Client::new(&server.uri(), ClientOptions { token: Some("user-token".to_string()), ..Default::default() }) .unwrap(); let err = c.list_accounts().await.unwrap_err(); assert!(matches!(err, MciasError::Forbidden(_))); } // ---- integration: login → validate → logout ---- #[tokio::test] async fn test_integration_login_validate_logout() { let server = MockServer::start().await; // Login Mock::given(method("POST")) .and(path("/v1/auth/login")) .respond_with(json_body(serde_json::json!({ "token": "integration-token", "expires_at": "2099-01-01T00:00:00Z" }))) .mount(&server) .await; // ValidateToken — valid Mock::given(method("POST")) .and(path("/v1/token/validate")) .respond_with(json_body(serde_json::json!({ "valid": true, "sub": "alice-uuid", "roles": [], "expires_at": "2099-01-01T00:00:00Z" }))) .up_to_n_times(1) .mount(&server) .await; // Logout Mock::given(method("POST")) .and(path("/v1/auth/logout")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; // ValidateToken — invalid (after logout, simulated by second mock) Mock::given(method("POST")) .and(path("/v1/token/validate")) .respond_with(json_body(serde_json::json!({"valid": false}))) .mount(&server) .await; let c = Client::new(&server.uri(), ClientOptions::default()).unwrap(); let (tok, _) = c.login("alice", "s3cret", None).await.unwrap(); assert_eq!(tok, "integration-token"); let claims = c.validate_token(&tok).await.unwrap(); assert!(claims.valid, "token should be valid before logout"); c.logout().await.unwrap(); assert!(c.token().await.is_none(), "token cleared after logout"); let claims_after = c.validate_token(&tok).await.unwrap(); assert!(!claims_after.valid, "token should be invalid after logout"); }