use super::*;
use crate::passkey::CredentialId;
use crate::passkey::main::types::AuthenticatorAttestationResponse;
use crate::storage::{CacheKey, CachePrefix};
use ciborium::value::Value as CborValue;
#[test]
fn test_parse_attestation_object_success_none_fmt() {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let expected_fmt = "none".to_string();
let expected_auth_data = b"authdata".to_vec();
let expected_att_stmt_map = Vec::<(CborValue, CborValue)>::new();
let cbor_map = vec![
(
CborValue::Text("fmt".to_string()),
CborValue::Text(expected_fmt.clone()),
),
(
CborValue::Text("attStmt".to_string()),
CborValue::Map(expected_att_stmt_map.clone()),
),
(
CborValue::Text("authData".to_string()),
CborValue::Bytes(expected_auth_data.clone()),
),
];
let cbor_value = CborValue::Map(cbor_map);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).expect("CBOR serialization failed");
let attestation_base64 = URL_SAFE_NO_PAD.encode(&cbor_bytes);
let result = parse_attestation_object(&attestation_base64);
assert!(
result.is_ok(),
"Parsing failed for input '{}': {:?}",
attestation_base64,
result.err()
);
let att_obj = result.unwrap();
assert_eq!(att_obj.fmt, expected_fmt);
assert_eq!(att_obj.auth_data, expected_auth_data);
assert_eq!(
att_obj.att_stmt, expected_att_stmt_map,
"attStmt should be an empty map"
);
}
#[test]
fn test_parse_attestation_object_invalid_base64() {
let attestation_base64 = "not-valid-base64!@#";
let result = parse_attestation_object(attestation_base64);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert!(msg.contains("Failed to decode attestation object"));
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_parse_attestation_object_valid_base64_invalid_cbor() {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let attestation_base64 = URL_SAFE_NO_PAD.encode(b"this is not cbor");
let result = parse_attestation_object(&attestation_base64);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert!(
msg.contains("Invalid CBOR data"),
"Error message was: {msg}"
);
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_parse_attestation_object_cbor_map_missing_fmt() {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let cbor_map = vec![
(
CborValue::Text("attStmt".to_string()),
CborValue::Map(Vec::new()),
),
(
CborValue::Text("authData".to_string()),
CborValue::Bytes(b"authdata".to_vec()),
),
];
let cbor_value = CborValue::Map(cbor_map);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).unwrap();
let attestation_base64 = URL_SAFE_NO_PAD.encode(&cbor_bytes);
let result = parse_attestation_object(&attestation_base64);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Missing required attestation data");
}
e => panic!("Expected PasskeyError::Format with specific message, got {e:?}"),
}
}
#[test]
fn test_parse_attestation_object_cbor_map_missing_auth_data() {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let cbor_map = vec![
(
CborValue::Text("fmt".to_string()),
CborValue::Text("none".to_string()),
),
(
CborValue::Text("attStmt".to_string()),
CborValue::Map(Vec::new()),
),
];
let cbor_value = CborValue::Map(cbor_map);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).unwrap();
let attestation_base64 = URL_SAFE_NO_PAD.encode(&cbor_bytes);
let result = parse_attestation_object(&attestation_base64);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Missing required attestation data");
}
e => panic!("Expected PasskeyError::Format with specific message, got {e:?}"),
}
}
#[test]
fn test_parse_attestation_object_cbor_map_missing_att_stmt() {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let cbor_map = vec![
(
CborValue::Text("fmt".to_string()),
CborValue::Text("none".to_string()),
),
(
CborValue::Text("authData".to_string()),
CborValue::Bytes(b"authdata".to_vec()),
),
];
let cbor_value = CborValue::Map(cbor_map);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).unwrap();
let attestation_base64 = URL_SAFE_NO_PAD.encode(&cbor_bytes);
let result = parse_attestation_object(&attestation_base64);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Missing required attestation data");
}
e => panic!("Expected PasskeyError::Format with specific message, got {e:?}"),
}
}
#[test]
fn test_parse_attestation_object_cbor_not_a_map() {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let cbor_value = CborValue::Array(vec![]);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).expect("CBOR serialization failed");
let attestation_base64 = URL_SAFE_NO_PAD.encode(&cbor_bytes);
let result = parse_attestation_object(&attestation_base64);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Invalid attestation format");
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_extract_key_coordinates_success() {
use ciborium::value::Integer;
let x_coord = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let y_coord = vec![
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
];
let cbor_map = vec![
(
CborValue::Integer(Integer::from(-2)), CborValue::Bytes(x_coord.clone()),
),
(
CborValue::Integer(Integer::from(-3)), CborValue::Bytes(y_coord.clone()),
),
];
let cbor_value = CborValue::Map(cbor_map);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).unwrap();
let result = extract_key_coordinates(&cbor_bytes);
assert!(result.is_ok());
let (extracted_x, extracted_y) = result.unwrap();
assert_eq!(extracted_x, x_coord);
assert_eq!(extracted_y, y_coord);
}
#[test]
fn test_extract_key_coordinates_missing_x() {
let y_coord = vec![16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
let cbor_map = vec![(
CborValue::Integer(Integer::from(-3)), CborValue::Bytes(y_coord.clone()),
)];
let cbor_value = CborValue::Map(cbor_map);
let mut credential_data = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut credential_data)
.expect("CBOR serialization failed");
let result = extract_key_coordinates(&credential_data);
assert!(
result.is_err(),
"Expected error but got success: {:?}",
result.ok()
);
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Missing or invalid key coordinates");
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_extract_key_coordinates_missing_y() {
let x_coord = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let cbor_map = vec![(
CborValue::Integer(Integer::from(-2)), CborValue::Bytes(x_coord.clone()),
)];
let cbor_value = CborValue::Map(cbor_map);
let mut credential_data = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut credential_data)
.expect("CBOR serialization failed");
let result = extract_key_coordinates(&credential_data);
assert!(
result.is_err(),
"Expected error but got success: {:?}",
result.ok()
);
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Missing or invalid key coordinates");
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_parse_credential_data_success() {
let mut auth_data = Vec::new();
auth_data.extend_from_slice(&[0u8; 32]);
auth_data.push(0x40);
auth_data.extend_from_slice(&[0, 0, 0, 0]);
auth_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
auth_data.extend_from_slice(&[0, 10]);
auth_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let public_key_bytes = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29];
auth_data.extend_from_slice(&public_key_bytes);
let result = parse_credential_data(&auth_data);
assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
let credential_data = result.unwrap();
assert_eq!(credential_data, &public_key_bytes);
}
#[test]
fn test_parse_credential_data_too_short() {
let mut auth_data = Vec::new();
auth_data.extend_from_slice(&[0u8; 32]);
auth_data.push(0x40);
let result = parse_credential_data(&auth_data);
assert!(
result.is_err(),
"Expected error but got success: {:?}",
result.ok()
);
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Authenticator data too short");
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_parse_credential_data_invalid_length() {
let mut auth_data = Vec::new();
auth_data.extend_from_slice(&[0u8; 32]);
auth_data.push(0x40);
auth_data.extend_from_slice(&[0, 0, 0, 0]);
auth_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
auth_data.extend_from_slice(&[0, 0]);
let result = parse_credential_data(&auth_data);
assert!(
result.is_err(),
"Expected error but got success: {:?}",
result.ok()
);
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Invalid credential ID length");
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_parse_credential_data_too_short_for_credential_id() {
let mut auth_data = Vec::new();
auth_data.extend_from_slice(&[0u8; 32]);
auth_data.push(0x40);
auth_data.extend_from_slice(&[0, 0, 0, 0]);
auth_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
auth_data.extend_from_slice(&[0, 20]);
auth_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let result = parse_credential_data(&auth_data);
assert!(
result.is_err(),
"Expected error but got success: {:?}",
result.ok()
);
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Authenticator data too short for credential ID");
}
e => panic!("Expected PasskeyError::Format, got {e:?}"),
}
}
#[test]
fn test_parse_credential_data_large_credential_id_length() {
let mut auth_data = Vec::new();
auth_data.extend_from_slice(&[0u8; 32]);
auth_data.push(0x40);
auth_data.extend_from_slice(&[0u8; 4]);
auth_data.extend_from_slice(&[0u8; 16]);
let large_cred_id_len = 1025u16;
auth_data.push((large_cred_id_len >> 8) as u8);
auth_data.push((large_cred_id_len & 0xFF) as u8);
auth_data.extend_from_slice(&[0xAAu8; 100]);
let result = parse_credential_data(&auth_data);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "Invalid credential ID length");
}
e => panic!("Expected PasskeyError::Format with 'Invalid credential ID length', got {e:?}"),
}
}
#[test]
fn test_extract_key_coordinates_invalid_cbor() {
let invalid_cbor_data = b"not valid cbor data";
let result = extract_key_coordinates(invalid_cbor_data);
assert!(result.is_err());
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert!(
msg.contains("Invalid public key format"),
"Error message was: {msg}"
);
}
e => panic!("Expected PasskeyError::Format with public key format error, got {e:?}"),
}
}
#[tokio::test]
async fn test_create_registration_options_integration() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::storage::GENERIC_CACHE_STORE;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "test_user_handle_456";
let user_info = crate::passkey::types::PublicKeyCredentialUserEntity {
user_handle: user_handle.to_string(),
name: "test_user_456".to_string(),
display_name: "Test User 456".to_string(),
};
let options = super::create_registration_options(user_info.clone(), vec![]).await;
assert!(options.is_ok(), "Failed to create registration options");
let registration_options = options.unwrap();
assert_eq!(registration_options.user.user_handle, user_handle);
assert_eq!(registration_options.user.name, "test_user_456");
assert_eq!(registration_options.user.display_name, "Test User 456");
let challenge_type = crate::passkey::types::ChallengeType::registration();
let challenge_id = crate::passkey::types::ChallengeId::new(user_handle.to_string()).unwrap();
let cache_result = super::get_and_validate_options(&challenge_type, &challenge_id).await;
assert!(
cache_result.is_ok(),
"Challenge was not stored in cache properly"
);
let (cache_prefix, cache_key) = (
CachePrefix::reg_challenge(),
CacheKey::new(user_handle.to_string()).unwrap(),
);
let cleanup_result = passkey_test_utils::remove_from_cache(cache_prefix, cache_key).await;
assert!(
cleanup_result.is_ok(),
"Failed to clean up test data from cache"
);
let cache_prefix = CachePrefix::new("regi_challenge".to_string()).unwrap();
let cache_key = CacheKey::new(user_handle.to_string()).unwrap();
let cache_get = GENERIC_CACHE_STORE
.lock()
.await
.get(cache_prefix, cache_key)
.await;
assert!(cache_get.is_ok(), "Error checking cache");
assert!(
cache_get.unwrap().is_none(),
"Cache entry should be removed"
);
}
#[tokio::test]
async fn test_get_or_create_user_handle() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::session::User as SessionUser;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let no_user_result = super::get_or_create_user_handle(&None).await;
assert!(
no_user_result.is_ok(),
"Failed to create user handle with no user"
);
let no_user_handle = no_user_result.unwrap();
assert!(
!no_user_handle.is_empty(),
"User handle should not be empty"
);
let session_user = Some(SessionUser {
id: "test_user_id_789".to_string(),
account: "test_account_789".to_string(),
label: "Test User 789".to_string(),
is_admin: false,
sequence_number: Some(1),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
});
let first_handle_result = super::get_or_create_user_handle(&session_user).await;
assert!(
first_handle_result.is_ok(),
"Failed to create user handle for logged-in user"
);
let first_handle = first_handle_result.unwrap();
let credential_id = "test_cred_id_for_user_handle";
let credential_data = passkey_test_utils::TestCredentialData::new(
credential_id,
"test_user_id_789", &first_handle, "Test User 789",
"Test Display Name 789",
"test_public_key",
"test_aaguid",
0,
);
let result = passkey_test_utils::insert_test_user_and_credential(credential_data).await;
assert!(result.is_ok(), "Failed to insert test user and credential");
let second_handle_result = super::get_or_create_user_handle(&session_user).await;
assert!(second_handle_result.is_ok());
let cleanup_result = passkey_test_utils::cleanup_test_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(cleanup_result.is_ok(), "Failed to clean up test credential");
}
#[tokio::test]
async fn test_verify_session_then_finish_registration_success() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::passkey::main::types::AuthenticatorAttestationResponse;
use crate::passkey::types::{PublicKeyCredentialUserEntity, SessionInfo};
use crate::session::User as SessionUser;
use crate::storage::{CacheKey, CachePrefix, get_data, remove_data, store_cache_keyed};
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_id = "test_user_12345";
let user_handle = "test_handle_12345";
let session_user = SessionUser {
id: user_id.to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: Some(1),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let session_info = SessionInfo {
user: session_user.clone(),
};
let cache_prefix = CachePrefix::session_info();
let cache_key = CacheKey::new(user_handle.to_string()).expect("Failed to create cache key");
let store_result =
store_cache_keyed::<_, PasskeyError>(cache_prefix, cache_key, session_info, 3600).await;
assert!(store_result.is_ok(), "Failed to store session info");
let user_entity = PublicKeyCredentialUserEntity {
user_handle: user_handle.to_string(),
name: "test_user".to_string(),
display_name: "Test User".to_string(),
};
let stored_options = crate::passkey::types::StoredOptions {
challenge: "test_challenge_12345".to_string(),
user: user_entity,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
ttl: 3600,
};
let cache_prefix = CachePrefix::reg_challenge();
let cache_key = CacheKey::new(user_handle.to_string()).expect("Failed to create cache key");
let challenge_store_result =
store_cache_keyed::<_, PasskeyError>(cache_prefix, cache_key, stored_options, 3600).await;
assert!(challenge_store_result.is_ok(), "Failed to store challenge");
let credential_id = "test_cred_verify_session_success";
let credential_data = passkey_test_utils::TestCredentialData::new(
credential_id,
user_id,
user_handle,
"test_user",
"Test User",
"test_public_key",
"test_aaguid",
0,
);
let user_creation_result =
passkey_test_utils::insert_test_user_and_credential(credential_data).await;
assert!(
user_creation_result.is_ok(),
"Failed to create test user and credential"
);
let client_data = super::WebAuthnClientData {
type_: "webauthn.create".to_string(),
challenge: "test_challenge_12345".to_string(),
origin: crate::passkey::config::ORIGIN.to_string(),
};
let client_data_json = serde_json::to_string(&client_data).unwrap();
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let mut cbor_map = Vec::new();
cbor_map.push((
CborValue::Text("fmt".to_string()),
CborValue::Text("none".to_string()),
));
cbor_map.push((
CborValue::Text("attStmt".to_string()),
CborValue::Map(Vec::new()),
));
let mut auth_data = Vec::new();
use ring::digest;
let rp_id_hash = digest::digest(
&digest::SHA256,
crate::passkey::config::PASSKEY_RP_ID.as_bytes(),
);
auth_data.extend_from_slice(rp_id_hash.as_ref());
auth_data.push(0x45);
auth_data.extend_from_slice(&[0u8; 4]);
auth_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
let cred_id_bytes = credential_id.as_bytes();
let cred_id_len = cred_id_bytes.len() as u16;
auth_data.extend_from_slice(&cred_id_len.to_be_bytes());
auth_data.extend_from_slice(cred_id_bytes);
let mock_public_key = vec![
0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x22, 0x58, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e,
0x3f, 0x40,
];
auth_data.extend_from_slice(&mock_public_key);
cbor_map.push((
CborValue::Text("authData".to_string()),
CborValue::Bytes(auth_data),
));
let cbor_value = CborValue::Map(cbor_map);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&cbor_value, &mut cbor_bytes).unwrap();
let attestation_object_b64 = crate::utils::base64url_encode(cbor_bytes).unwrap();
let reg_data = super::RegisterCredential {
raw_id: credential_id.to_string(),
id: credential_id.to_string(),
type_: "public-key".to_string(),
user_handle: Some(user_handle.to_string()),
response: AuthenticatorAttestationResponse {
client_data_json: client_data_b64,
attestation_object: attestation_object_b64,
},
};
let result = super::verify_session_then_finish_registration(session_user, reg_data).await;
assert!(
result.is_ok(),
"verify_session_then_finish_registration should succeed: {:?}",
result.err()
);
let (cache_prefix, cache_key) = (
CachePrefix::session_info(),
CacheKey::new(user_handle.to_string()).unwrap(),
);
let session_check = get_data::<SessionInfo, PasskeyError>(cache_prefix, cache_key).await;
assert!(session_check.is_ok());
assert!(
session_check.unwrap().is_none(),
"Session info should be removed from cache"
);
let cleanup_result = passkey_test_utils::cleanup_test_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(cleanup_result.is_ok(), "Failed to clean up test credential");
if let Ok(cache_key) = CacheKey::new(user_handle.to_string()) {
let cache_prefix = CachePrefix::reg_challenge();
let _ = remove_data::<PasskeyError>(cache_prefix, cache_key).await;
}
}
#[tokio::test]
async fn test_verify_session_then_finish_registration_missing_user_handle() {
use crate::passkey::main::types::AuthenticatorAttestationResponse;
use crate::session::User as SessionUser;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let session_user = SessionUser {
id: "test_user_missing_handle".to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: Some(1),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let reg_data = super::RegisterCredential {
raw_id: "test_cred_id".to_string(),
id: "test_cred_id".to_string(),
type_: "public-key".to_string(),
user_handle: None, response: AuthenticatorAttestationResponse {
client_data_json: "dummy".to_string(),
attestation_object: "dummy".to_string(),
},
};
let result = super::verify_session_then_finish_registration(session_user, reg_data).await;
assert!(result.is_err(), "Should fail when user handle is missing");
match result.err().unwrap() {
PasskeyError::ClientData(msg) => {
assert_eq!(msg, "User handle is missing");
}
e => panic!("Expected PasskeyError::ClientData, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_session_then_finish_registration_session_not_found() {
use crate::passkey::main::types::AuthenticatorAttestationResponse;
use crate::session::User as SessionUser;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "nonexistent_handle_12345";
let session_user = SessionUser {
id: "test_user_no_session".to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: Some(1),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let reg_data = super::RegisterCredential {
raw_id: "test_cred_id".to_string(),
id: "test_cred_id".to_string(),
type_: "public-key".to_string(),
user_handle: Some(user_handle.to_string()),
response: AuthenticatorAttestationResponse {
client_data_json: "dummy".to_string(),
attestation_object: "dummy".to_string(),
},
};
let result = super::verify_session_then_finish_registration(session_user, reg_data).await;
assert!(result.is_err(), "Should fail when session is not found");
match result.err().unwrap() {
PasskeyError::NotFound(msg) => {
assert_eq!(msg, "Session not found");
}
e => panic!("Expected PasskeyError::NotFound, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_session_then_finish_registration_user_id_mismatch() {
use crate::passkey::main::types::AuthenticatorAttestationResponse;
use crate::passkey::types::SessionInfo;
use crate::session::User as SessionUser;
use crate::storage::{CacheKey, CachePrefix, get_data, store_cache_keyed};
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "test_handle_mismatch";
let stored_session_user = SessionUser {
id: "stored_user_id".to_string(),
account: "test_account".to_string(),
label: "Stored User".to_string(),
is_admin: false,
sequence_number: Some(1),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let current_session_user = SessionUser {
id: "current_user_id".to_string(), account: "test_account".to_string(),
label: "Current User".to_string(),
is_admin: false,
sequence_number: Some(1),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let session_info = SessionInfo {
user: stored_session_user,
};
let cache_prefix = CachePrefix::session_info();
let cache_key = CacheKey::new(user_handle.to_string()).expect("Failed to create cache key");
let store_result =
store_cache_keyed::<_, PasskeyError>(cache_prefix, cache_key, session_info, 3600).await;
assert!(store_result.is_ok(), "Failed to store session info");
let reg_data = super::RegisterCredential {
raw_id: "test_cred_id".to_string(),
id: "test_cred_id".to_string(),
type_: "public-key".to_string(),
user_handle: Some(user_handle.to_string()),
response: AuthenticatorAttestationResponse {
client_data_json: "dummy".to_string(),
attestation_object: "dummy".to_string(),
},
};
let result =
super::verify_session_then_finish_registration(current_session_user, reg_data).await;
assert!(
result.is_err(),
"Should fail when user IDs don't match - this prevents session hijacking"
);
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert_eq!(msg, "User ID mismatch");
}
e => panic!("Expected PasskeyError::Format for user ID mismatch, got {e:?}"),
}
let (cache_prefix, cache_key) = (
CachePrefix::session_info(),
CacheKey::new(user_handle.to_string()).unwrap(),
);
let session_check = get_data::<SessionInfo, PasskeyError>(cache_prefix, cache_key).await;
assert!(session_check.is_ok());
assert!(
session_check.unwrap().is_none(),
"Session info should be removed even on security failure"
);
}
fn create_test_register_credential_for_verify_client_data(
client_data_json: String,
user_handle: Option<String>,
) -> RegisterCredential {
RegisterCredential {
raw_id: "test_cred_id".to_string(),
id: "test_cred_id".to_string(),
type_: "public-key".to_string(),
user_handle,
response: AuthenticatorAttestationResponse {
client_data_json,
attestation_object: "test_attestation_object".to_string(),
},
}
}
fn create_test_client_data_json(type_: &str, challenge: &str, origin: &str) -> String {
let client_data = super::WebAuthnClientData {
type_: type_.to_string(),
challenge: challenge.to_string(),
origin: origin.to_string(),
};
serde_json::to_string(&client_data).unwrap()
}
#[tokio::test]
async fn test_verify_client_data_success() {
use crate::storage::{CacheKey, CachePrefix, store_cache_keyed};
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "test_user_verify_client_data_success";
let challenge = "test_challenge_verify_success";
let stored_options = StoredOptions {
challenge: challenge.to_string(),
user: PublicKeyCredentialUserEntity {
user_handle: user_handle.to_string(),
name: "test_user".to_string(),
display_name: "Test User".to_string(),
},
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
ttl: 3600,
};
let cache_prefix = CachePrefix::reg_challenge();
let cache_key = CacheKey::new(user_handle.to_string()).expect("Failed to create cache key");
let store_result =
store_cache_keyed::<_, PasskeyError>(cache_prefix, cache_key, stored_options, 3600).await;
assert!(store_result.is_ok(), "Failed to store challenge in cache");
let client_data_json = create_test_client_data_json(
"webauthn.create",
challenge,
&crate::passkey::config::ORIGIN,
);
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
client_data_b64,
Some(user_handle.to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(
result.is_ok(),
"verify_client_data should succeed with valid data: {:?}",
result.err()
);
if let Ok(cache_key) = CacheKey::new(user_handle.to_string()) {
let cache_prefix = CachePrefix::reg_challenge();
let _ = remove_data::<PasskeyError>(cache_prefix, cache_key).await;
}
}
#[tokio::test]
async fn test_verify_client_data_invalid_base64() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let reg_data = create_test_register_credential_for_verify_client_data(
"invalid_base64!@#$%".to_string(),
Some("test_user".to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with invalid base64");
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert!(msg.contains("Failed to decode client data"));
}
e => panic!("Expected PasskeyError::Format for invalid base64, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_client_data_invalid_utf8() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let invalid_utf8_bytes = vec![0xFF, 0xFE, 0xFD]; let invalid_utf8_b64 = crate::utils::base64url_encode(invalid_utf8_bytes).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
invalid_utf8_b64,
Some("test_user".to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with invalid UTF-8");
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert!(msg.contains("Client data is not valid UTF-8"));
}
e => panic!("Expected PasskeyError::Format for invalid UTF-8, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_client_data_invalid_json() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let invalid_json = "{ invalid json structure }";
let invalid_json_b64 =
crate::utils::base64url_encode(invalid_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
invalid_json_b64,
Some("test_user".to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with invalid JSON");
match result.err().unwrap() {
PasskeyError::Format(msg) => {
assert!(msg.contains("Failed to parse client data JSON"));
}
e => panic!("Expected PasskeyError::Format for invalid JSON, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_client_data_wrong_type() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let client_data_json = create_test_client_data_json(
"webauthn.get", "test_challenge",
&crate::passkey::config::ORIGIN,
);
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
client_data_b64,
Some("test_user".to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with wrong client data type");
match result.err().unwrap() {
PasskeyError::ClientData(msg) => {
assert_eq!(msg, "Invalid type");
}
e => panic!("Expected PasskeyError::ClientData for wrong type, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_client_data_missing_user_handle() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let client_data_json = create_test_client_data_json(
"webauthn.create",
"test_challenge",
&crate::passkey::config::ORIGIN,
);
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
client_data_b64,
None, );
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with missing user handle");
match result.err().unwrap() {
PasskeyError::ClientData(msg) => {
assert!(msg.contains("User handle is missing"));
}
e => panic!("Expected PasskeyError::ClientData for missing user handle, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_client_data_challenge_not_found() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "test_user_challenge_not_found";
let client_data_json = create_test_client_data_json(
"webauthn.create",
"test_challenge",
&crate::passkey::config::ORIGIN,
);
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
client_data_b64,
Some(user_handle.to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(
result.is_err(),
"Should fail when challenge not found in cache"
);
match result.err().unwrap() {
PasskeyError::NotFound(_) => {
}
e => panic!("Expected PasskeyError::NotFound for missing challenge, got {e:?}"),
}
}
#[tokio::test]
async fn test_verify_client_data_challenge_mismatch() {
use crate::storage::{CacheKey, CachePrefix, store_cache_keyed};
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "test_user_challenge_mismatch";
let stored_challenge = "stored_challenge_123";
let client_challenge = "different_challenge_456";
let stored_options = StoredOptions {
challenge: stored_challenge.to_string(),
user: PublicKeyCredentialUserEntity {
user_handle: user_handle.to_string(),
name: "test_user".to_string(),
display_name: "Test User".to_string(),
},
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
ttl: 3600,
};
let cache_prefix = CachePrefix::reg_challenge();
let cache_key = CacheKey::new(user_handle.to_string()).expect("Failed to create cache key");
let store_result =
store_cache_keyed::<_, PasskeyError>(cache_prefix, cache_key, stored_options, 3600).await;
assert!(store_result.is_ok(), "Failed to store challenge in cache");
let client_data_json = create_test_client_data_json(
"webauthn.create",
client_challenge, &crate::passkey::config::ORIGIN,
);
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
client_data_b64,
Some(user_handle.to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with challenge mismatch");
match result.err().unwrap() {
PasskeyError::Challenge(msg) => {
assert!(msg.contains("Challenge verification failed"));
}
e => panic!("Expected PasskeyError::Challenge for challenge mismatch, got {e:?}"),
}
if let Ok(cache_key) = CacheKey::new(user_handle.to_string()) {
let cache_prefix = CachePrefix::reg_challenge();
let _ = remove_data::<PasskeyError>(cache_prefix, cache_key).await;
}
}
#[tokio::test]
async fn test_verify_client_data_origin_mismatch() {
use crate::storage::{CacheKey, CachePrefix, store_cache_keyed};
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_handle = "test_user_origin_mismatch";
let challenge = "test_challenge_origin";
let stored_options = StoredOptions {
challenge: challenge.to_string(),
user: PublicKeyCredentialUserEntity {
user_handle: user_handle.to_string(),
name: "test_user".to_string(),
display_name: "Test User".to_string(),
},
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
ttl: 3600,
};
let cache_prefix = CachePrefix::reg_challenge();
let cache_key = CacheKey::new(user_handle.to_string()).expect("Failed to create cache key");
let store_result =
store_cache_keyed::<_, PasskeyError>(cache_prefix, cache_key, stored_options, 3600).await;
assert!(store_result.is_ok(), "Failed to store challenge in cache");
let client_data_json = create_test_client_data_json(
"webauthn.create",
challenge,
"https://evil-site.com", );
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
let reg_data = create_test_register_credential_for_verify_client_data(
client_data_b64,
Some(user_handle.to_string()),
);
let result = super::verify_client_data(®_data).await;
assert!(result.is_err(), "Should fail with origin mismatch");
match result.err().unwrap() {
PasskeyError::ClientData(msg) => {
assert!(msg.contains("Invalid origin"));
assert!(msg.contains("https://evil-site.com"));
}
e => panic!("Expected PasskeyError::ClientData for origin mismatch, got {e:?}"),
}
if let Ok(cache_key) = CacheKey::new(user_handle.to_string()) {
let cache_prefix = CachePrefix::reg_challenge();
let _ = remove_data::<PasskeyError>(cache_prefix, cache_key).await;
}
}
fn create_test_register_credential_for_extract_credential_public_key() -> RegisterCredential {
let test_origin = crate::test_utils::get_test_origin();
let client_data_json =
create_test_client_data_json("webauthn.create", "test-challenge", &test_origin);
let client_data_b64 =
crate::utils::base64url_encode(client_data_json.as_bytes().to_vec()).unwrap();
RegisterCredential {
id: "test-credential-id".to_string(),
raw_id: "dGVzdC1jcmVkZW50aWFsLWlk".to_string(),
type_: "public-key".to_string(),
user_handle: Some("test-user-handle".to_string()),
response: AuthenticatorAttestationResponse {
attestation_object: create_simple_test_attestation_object().unwrap(),
client_data_json: client_data_b64,
},
}
}
fn create_simple_test_attestation_object() -> Result<String, String> {
let mut cose_key = Vec::new();
let mut cbor_map = Vec::new();
cbor_map.push((
CborValue::Integer(Integer::from(1)),
CborValue::Integer(Integer::from(2)),
));
cbor_map.push((
CborValue::Integer(Integer::from(3)),
CborValue::Integer(Integer::from(-7)),
));
cbor_map.push((
CborValue::Integer(Integer::from(-1)),
CborValue::Integer(Integer::from(1)),
));
let x_coord = vec![
0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, 0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef, 0xff,
0x0f, 0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, 0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef,
0xff, 0x0f,
];
cbor_map.push((
CborValue::Integer(Integer::from(-2)),
CborValue::Bytes(x_coord),
));
let y_coord = vec![
0x0f, 0xff, 0xef, 0xdf, 0xcf, 0xbf, 0xaf, 0x9f, 0x8f, 0x7f, 0x6f, 0x5f, 0x4f, 0x3f, 0x2f,
0x1f, 0x0f, 0xff, 0xef, 0xdf, 0xcf, 0xbf, 0xaf, 0x9f, 0x8f, 0x7f, 0x6f, 0x5f, 0x4f, 0x3f,
0x2f, 0x1f,
];
cbor_map.push((
CborValue::Integer(Integer::from(-3)),
CborValue::Bytes(y_coord),
));
let cose_key_cbor = CborValue::Map(cbor_map);
ciborium::ser::into_writer(&cose_key_cbor, &mut cose_key)
.map_err(|e| format!("Failed to serialize COSE key: {e}"))?;
let credential_id = vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10,
];
let mut auth_data = Vec::new();
use ring::digest;
let rp_id_hash = digest::digest(
&digest::SHA256,
crate::passkey::config::PASSKEY_RP_ID.as_bytes(),
);
auth_data.extend_from_slice(rp_id_hash.as_ref());
auth_data.push(0x45);
auth_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
auth_data.extend_from_slice(&[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
]);
let cred_id_len = credential_id.len() as u16;
auth_data.push((cred_id_len >> 8) as u8);
auth_data.push((cred_id_len & 0xff) as u8);
auth_data.extend_from_slice(&credential_id);
auth_data.extend_from_slice(&cose_key);
let attestation_cbor = vec![
0xa3, 0x63, 0x66, 0x6d, 0x74, 0x64, 0x6e, 0x6f, 0x6e, 0x65, 0x67, 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, 0xa0, 0x68, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, ];
let mut full_attestation = attestation_cbor;
if auth_data.len() < 24 {
full_attestation.push(0x40 + auth_data.len() as u8);
} else if auth_data.len() < 256 {
full_attestation.push(0x58);
full_attestation.push(auth_data.len() as u8);
} else {
full_attestation.push(0x59);
full_attestation.push((auth_data.len() >> 8) as u8);
full_attestation.push((auth_data.len() & 0xff) as u8);
}
full_attestation.extend_from_slice(&auth_data);
crate::utils::base64url_encode(full_attestation).map_err(|e| e.to_string())
}
#[tokio::test]
async fn test_extract_credential_public_key_success() {
crate::test_utils::init_test_environment().await;
let reg_data = create_test_register_credential_for_extract_credential_public_key();
let result = extract_credential_public_key(®_data);
match &result {
Ok(key) => println!("Success: got public key of length {}", key.len()),
Err(e) => println!("Error: {e}"),
}
assert!(result.is_ok());
let public_key = result.unwrap();
assert!(!public_key.is_empty());
}
#[test]
fn test_extract_credential_public_key_invalid_client_data() {
let mut reg_data = create_test_register_credential_for_extract_credential_public_key();
reg_data.response.client_data_json = "invalid-base64!@#".to_string();
let result = extract_credential_public_key(®_data);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to decode client data")
);
}
#[test]
fn test_extract_credential_public_key_invalid_attestation_object() {
let mut reg_data = create_test_register_credential_for_extract_credential_public_key();
reg_data.response.attestation_object = "invalid-base64!@#".to_string();
let result = extract_credential_public_key(®_data);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to decode attestation object")
);
}
#[test]
fn test_extract_credential_public_key_malformed_attestation_object() {
let mut reg_data = create_test_register_credential_for_extract_credential_public_key();
reg_data.response.attestation_object = base64url_encode(b"not-valid-cbor".to_vec()).unwrap();
let result = extract_credential_public_key(®_data);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid CBOR data")
);
}
#[tokio::test]
async fn test_multiple_same_aaguid_credentials_coexist() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::passkey::types::CredentialSearchField;
use crate::session::UserId;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let shared_aaguid = "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4";
let user_id_str = "test_user_aaguid_coexist";
let user_handle = "test_handle_aaguid_coexist";
let cred1_data = passkey_test_utils::TestCredentialData::new(
"cred_aaguid_coexist_1",
user_id_str,
user_handle,
"User AAGUID Test",
"User AAGUID Test Display",
"test_public_key_1",
shared_aaguid,
0,
);
passkey_test_utils::insert_test_user_and_credential(cred1_data)
.await
.expect("Failed to insert first credential");
let cred2_data = passkey_test_utils::TestCredentialData::new(
"cred_aaguid_coexist_2",
user_id_str,
user_handle,
"User AAGUID Test",
"User AAGUID Test Display",
"test_public_key_2",
shared_aaguid,
0,
);
passkey_test_utils::insert_test_credential(cred2_data)
.await
.expect("Failed to insert second credential");
let validated_data = super::ValidatedRegistrationData {
public_key: "test_public_key_3".to_string(),
user_handle: user_handle.to_string(),
stored_user: crate::passkey::types::PublicKeyCredentialUserEntity {
user_handle: user_handle.to_string(),
name: "User AAGUID Test".to_string(),
display_name: "User AAGUID Test Display".to_string(),
},
credential_id: "cred_aaguid_coexist_3".to_string(),
aaguid: shared_aaguid.to_string(),
rp_id: "localhost".to_string(),
};
let user_id = UserId::new(user_id_str.to_string()).expect("Valid user ID");
let new_credential = super::prepare_registration_storage(user_id.clone(), validated_data)
.await
.expect("prepare_registration_storage should succeed");
assert_eq!(new_credential.credential_id, "cred_aaguid_coexist_3");
assert_eq!(new_credential.aaguid, shared_aaguid);
assert_eq!(new_credential.user_id, user_id_str);
let all_creds =
PasskeyStore::get_credentials_by(CredentialSearchField::UserId(user_id.clone()))
.await
.expect("Failed to get credentials");
let cred1_exists = all_creds
.iter()
.any(|c| c.credential_id == "cred_aaguid_coexist_1");
let cred2_exists = all_creds
.iter()
.any(|c| c.credential_id == "cred_aaguid_coexist_2");
assert!(
cred1_exists,
"First credential should still exist (not deleted by AAGUID match)"
);
assert!(
cred2_exists,
"Second credential should still exist (not deleted by AAGUID match)"
);
for cred_id in ["cred_aaguid_coexist_1", "cred_aaguid_coexist_2"] {
let _ = passkey_test_utils::cleanup_test_credential(
CredentialId::new(cred_id.to_string()).expect("Valid credential ID"),
)
.await;
}
}
#[tokio::test]
async fn test_start_registration_populates_exclude_credentials() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::session::User as SessionUser;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_id_str = "test_user_exclude_creds";
let user_handle = "test_handle_exclude_creds";
let cred1_data = passkey_test_utils::TestCredentialData::new(
"cred_exclude_test_1",
user_id_str,
user_handle,
"Exclude Creds User",
"Exclude Creds Display",
"test_pk_ec1",
"aaguid-google-pm",
0,
);
passkey_test_utils::insert_test_user_and_credential(cred1_data)
.await
.expect("Failed to insert first credential");
let cred2_data = passkey_test_utils::TestCredentialData::new(
"cred_exclude_test_2",
user_id_str,
user_handle,
"Exclude Creds User",
"Exclude Creds Display",
"test_pk_ec2",
"aaguid-yubikey-5c",
0,
);
passkey_test_utils::insert_test_credential(cred2_data)
.await
.expect("Failed to insert second credential");
let session_user = Some(SessionUser {
id: user_id_str.to_string(),
account: "exclude_creds_account".to_string(),
label: "Exclude Creds User".to_string(),
is_admin: false,
sequence_number: Some(99),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
});
let options = super::start_registration(
session_user,
"exclude_creds_user".to_string(),
"Exclude Creds Display".to_string(),
)
.await
.expect("start_registration should succeed");
let exclude_ids: Vec<&str> = options
.exclude_credentials
.iter()
.map(|ec| ec.id.as_str())
.collect();
assert!(
exclude_ids.contains(&"cred_exclude_test_1"),
"excludeCredentials should contain first credential ID, got: {:?}",
exclude_ids
);
assert!(
exclude_ids.contains(&"cred_exclude_test_2"),
"excludeCredentials should contain second credential ID, got: {:?}",
exclude_ids
);
for ec in &options.exclude_credentials {
assert_eq!(
ec.type_, "public-key",
"excludeCredentials type should be 'public-key'"
);
}
for cred_id in ["cred_exclude_test_1", "cred_exclude_test_2"] {
let _ = passkey_test_utils::cleanup_test_credential(
CredentialId::new(cred_id.to_string()).expect("Valid credential ID"),
)
.await;
}
let cache_prefix = CachePrefix::reg_challenge();
let cache_key = CacheKey::new(options.user.user_handle.clone()).unwrap();
let _ = passkey_test_utils::remove_from_cache(cache_prefix, cache_key).await;
let cache_prefix = CachePrefix::session_info();
let cache_key = CacheKey::new(options.user.user_handle).unwrap();
let _ = passkey_test_utils::remove_from_cache(cache_prefix, cache_key).await;
}
#[tokio::test]
async fn test_start_registration_anonymous_has_empty_exclude_credentials() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let options =
super::start_registration(None, "anon_user".to_string(), "Anonymous User".to_string())
.await
.expect("start_registration should succeed for anonymous user");
assert!(
options.exclude_credentials.is_empty(),
"excludeCredentials should be empty for anonymous users, got: {:?}",
options.exclude_credentials
);
let cache_prefix = CachePrefix::reg_challenge();
let cache_key = CacheKey::new(options.user.user_handle).unwrap();
let _ = crate::passkey::main::test_utils::remove_from_cache(cache_prefix, cache_key).await;
}