use crate::{canonicalize_string, parse, AadContext, AadError};
use sha2::{Digest, Sha256};
fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
#[test]
fn test_vector_10_1_minimal_fields() {
let input = r#"{
"v": 1,
"tenant": "org_abc",
"resource": "secrets/db",
"purpose": "encryption"
}"#;
let expected_canonical =
r#"{"purpose":"encryption","resource":"secrets/db","tenant":"org_abc","v":1}"#;
let expected_sha256 = "03fdc63d2f82815eb0a97e6f1a02890e152c021a795142b9c22e2b31a3bd83eb";
let expected_hex = "7b22707572706f7365223a22656e6372797074696f6e222c227265736f75726365223a22736563726574732f6462222c2274656e616e74223a226f72675f616263222c2276223a317d";
let canonical = canonicalize_string(input).expect("should parse and canonicalize");
assert_eq!(canonical, expected_canonical);
let utf8_hex = hex::encode(canonical.as_bytes());
assert_eq!(utf8_hex, expected_hex);
let sha256 = sha256_hex(canonical.as_bytes());
assert_eq!(sha256, expected_sha256);
}
#[test]
fn test_vector_10_1_via_builder() {
let ctx =
AadContext::new("org_abc", "secrets/db", "encryption").expect("should create context");
let canonical = ctx.canonicalize_string().expect("should canonicalize");
let expected = r#"{"purpose":"encryption","resource":"secrets/db","tenant":"org_abc","v":1}"#;
assert_eq!(canonical, expected);
}
#[test]
fn test_vector_10_2_all_fields() {
let input = r#"{
"v": 1,
"tenant": "org_abc",
"resource": "secrets/db/prod",
"purpose": "encryption-at-rest",
"ts": 1706400000
}"#;
let expected_canonical = r#"{"purpose":"encryption-at-rest","resource":"secrets/db/prod","tenant":"org_abc","ts":1706400000,"v":1}"#;
let canonical = canonicalize_string(input).expect("should parse and canonicalize");
assert_eq!(canonical, expected_canonical);
}
#[test]
fn test_vector_10_2_via_builder() {
let ctx = AadContext::new("org_abc", "secrets/db/prod", "encryption-at-rest")
.expect("should create context")
.with_timestamp(1706400000)
.expect("should add timestamp");
let canonical = ctx.canonicalize_string().expect("should canonicalize");
let expected = r#"{"purpose":"encryption-at-rest","resource":"secrets/db/prod","tenant":"org_abc","ts":1706400000,"v":1}"#;
assert_eq!(canonical, expected);
}
#[test]
fn test_vector_10_3_unicode() {
let input = r#"{
"v": 1,
"tenant": "组织_测试",
"resource": "data/🔐/secret",
"purpose": "encryption"
}"#;
let expected_canonical =
r#"{"purpose":"encryption","resource":"data/🔐/secret","tenant":"组织_测试","v":1}"#;
let canonical = canonicalize_string(input).expect("should parse and canonicalize");
assert_eq!(canonical, expected_canonical);
assert!(canonical.contains("组织_测试"));
assert!(canonical.contains("🔐"));
}
#[test]
fn test_vector_10_3_via_builder() {
let ctx = AadContext::new("组织_测试", "data/🔐/secret", "encryption")
.expect("should create context");
let canonical = ctx.canonicalize_string().expect("should canonicalize");
assert!(canonical.contains("组织_测试"));
assert!(canonical.contains("🔐"));
}
#[test]
fn test_vector_10_4_extension_fields() {
let input = r#"{
"v": 1,
"tenant": "org_abc",
"resource": "vault/key",
"purpose": "key-wrapping",
"x_vault_cluster": "us-east-1"
}"#;
let expected_canonical = r#"{"purpose":"key-wrapping","resource":"vault/key","tenant":"org_abc","v":1,"x_vault_cluster":"us-east-1"}"#;
let canonical = canonicalize_string(input).expect("should parse and canonicalize");
assert_eq!(canonical, expected_canonical);
}
#[test]
fn test_vector_10_4_via_builder() {
let ctx = AadContext::new("org_abc", "vault/key", "key-wrapping")
.expect("should create context")
.with_string_extension("x_vault_cluster", "us-east-1")
.expect("should add extension");
let canonical = ctx.canonicalize_string().expect("should canonicalize");
let expected = r#"{"purpose":"key-wrapping","resource":"vault/key","tenant":"org_abc","v":1,"x_vault_cluster":"us-east-1"}"#;
assert_eq!(canonical, expected);
}
#[test]
fn test_vector_10_5_jcs_edge_cases() {
let input = r#"{
"v": 1,
"tenant": "org\u000Atest",
"resource": "path/with\"quotes",
"purpose": "test",
"ts": 9007199254740991
}"#;
let expected_canonical = r#"{"purpose":"test","resource":"path/with\"quotes","tenant":"org\ntest","ts":9007199254740991,"v":1}"#;
let canonical = canonicalize_string(input).expect("should parse and canonicalize");
assert_eq!(canonical, expected_canonical);
assert!(canonical.contains(r#"\n"#));
assert!(canonical.contains(r#"\""#));
assert!(canonical.contains("9007199254740991"));
}
#[test]
fn test_vector_10_5_max_safe_integer() {
let ctx = AadContext::new("org", "res", "test")
.expect("should create context")
.with_timestamp(9007199254740991)
.expect("should accept max safe integer");
let canonical = ctx.canonicalize_string().expect("should canonicalize");
assert!(canonical.contains("9007199254740991"));
}
#[test]
fn test_negative_integer_above_max_safe() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","ts":9007199254740992}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::IntegerOutOfRange { .. })));
}
#[test]
fn test_negative_integer_value() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","ts":-1}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::NegativeInteger { .. })));
}
#[test]
fn test_negative_empty_tenant() {
let input = r#"{"v":1,"tenant":"","resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::FieldTooShort { field: "tenant", .. })));
}
#[test]
fn test_negative_nul_byte_in_tenant() {
let input = r#"{"v":1,"tenant":"org\u0000abc","resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::NulByteInValue { field: "tenant" })));
}
#[test]
fn test_negative_unknown_field() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","unknown":"value"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::UnknownField { .. })));
}
#[test]
fn test_negative_missing_required_field() {
let input = r#"{"v":1,"tenant":"org","resource":"res"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::MissingRequiredField { field: "purpose" })));
}
#[test]
fn test_negative_duplicate_key() {
let input = r#"{"v":1,"tenant":"org","tenant":"other","resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::DuplicateKey { .. })));
}
#[test]
fn test_negative_invalid_extension_key_single_underscore() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","x_foo":"value"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::InvalidExtensionKeyFormat { .. })));
}
#[test]
fn test_negative_invalid_extension_key_empty_app() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","x__field":"value"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::InvalidExtensionKeyFormat { .. })));
}
#[test]
fn test_negative_invalid_extension_key_empty_field() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","x_app_":"value"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::InvalidExtensionKeyFormat { .. })));
}
#[test]
fn test_negative_unsupported_version() {
let input = r#"{"v":2,"tenant":"org","resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::UnsupportedVersion { version: 2 })));
}
#[test]
fn test_negative_version_zero() {
let input = r#"{"v":0,"tenant":"org","resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::UnsupportedVersion { version: 0 })));
}
#[test]
fn test_negative_wrong_field_type_version() {
let input = r#"{"v":"1","tenant":"org","resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::WrongFieldType { field: "v", .. })));
}
#[test]
fn test_negative_wrong_field_type_tenant() {
let input = r#"{"v":1,"tenant":123,"resource":"res","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::WrongFieldType { field: "tenant", .. })));
}
#[test]
fn test_negative_wrong_field_type_ts() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","ts":"1706400000"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::WrongFieldType { field: "ts", .. })));
}
#[test]
fn test_negative_invalid_json() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test""#; let result = parse(input);
assert!(matches!(result, Err(AadError::InvalidJson { .. })));
}
#[test]
fn test_negative_not_an_object() {
let input = r#"["v", 1, "tenant", "org"]"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::InvalidJson { .. })));
}
#[test]
fn test_extension_field_with_integer_value() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","x_app_count":42}"#;
let result = parse(input);
assert!(result.is_ok());
let ctx = result.unwrap();
assert_eq!(ctx.extensions().len(), 1);
}
#[test]
fn test_multiple_extension_fields() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","x_app_field":"value","x_other_key":"data"}"#;
let result = parse(input);
assert!(result.is_ok());
let ctx = result.unwrap();
assert_eq!(ctx.extensions().len(), 2);
let canonical = ctx.canonicalize_string().unwrap();
let app_pos = canonical.find("x_app_field").unwrap();
let other_pos = canonical.find("x_other_key").unwrap();
assert!(app_pos < other_pos); }
#[test]
fn test_extension_key_with_multiple_underscores_in_field() {
let input =
r#"{"v":1,"tenant":"org","resource":"res","purpose":"test","x_app_field_name":"value"}"#;
let result = parse(input);
assert!(result.is_ok());
}
#[test]
fn test_tenant_at_max_length() {
let tenant = "a".repeat(256);
let input = format!(r#"{{"v":1,"tenant":"{}","resource":"res","purpose":"test"}}"#, tenant);
let result = parse(&input);
assert!(result.is_ok());
}
#[test]
fn test_tenant_exceeds_max_length() {
let tenant = "a".repeat(257);
let input = format!(r#"{{"v":1,"tenant":"{}","resource":"res","purpose":"test"}}"#, tenant);
let result = parse(&input);
assert!(matches!(result, Err(AadError::FieldTooLong { field: "tenant", .. })));
}
#[test]
fn test_resource_at_max_length() {
let resource = "a".repeat(1024);
let input = format!(r#"{{"v":1,"tenant":"org","resource":"{}","purpose":"test"}}"#, resource);
let result = parse(&input);
assert!(result.is_ok());
}
#[test]
fn test_resource_exceeds_max_length() {
let resource = "a".repeat(1025);
let input = format!(r#"{{"v":1,"tenant":"org","resource":"{}","purpose":"test"}}"#, resource);
let result = parse(&input);
assert!(matches!(result, Err(AadError::FieldTooLong { field: "resource", .. })));
}
#[test]
fn test_whitespace_in_input_is_handled() {
let input = r#"
{
"v" : 1 ,
"tenant" : "org" ,
"resource" : "res" ,
"purpose" : "test"
}
"#;
let canonical = canonicalize_string(input).unwrap();
assert!(!canonical.contains(' '));
assert!(!canonical.contains('\n'));
assert_eq!(canonical, r#"{"purpose":"test","resource":"res","tenant":"org","v":1}"#);
}
#[test]
fn test_special_characters_in_strings() {
let input = r#"{"v":1,"tenant":"org/abc","resource":"path\\to\\file","purpose":"test\ttab"}"#;
let result = parse(input);
assert!(result.is_ok());
let ctx = result.unwrap();
assert_eq!(ctx.tenant(), "org/abc");
assert_eq!(ctx.resource(), "path\\to\\file");
assert_eq!(ctx.purpose(), "test\ttab");
}
#[test]
fn test_empty_resource() {
let input = r#"{"v":1,"tenant":"org","resource":"","purpose":"test"}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::FieldTooShort { field: "resource", .. })));
}
#[test]
fn test_empty_purpose() {
let input = r#"{"v":1,"tenant":"org","resource":"res","purpose":""}"#;
let result = parse(input);
assert!(matches!(result, Err(AadError::FieldTooShort { field: "purpose", .. })));
}