bud11_conformance.rs (14567B)
1 #![cfg(feature = "serde")] 2 3 use radroots_blossom::{ 4 Sha256, 5 authorization::{ 6 AuthoredUploadClaim, AuthorizationAction, AuthorizationClaim, AuthorizationContent, 7 AuthorizationTarget, AuthorizationValidation, RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, 8 ServerDomain, ServerScopeRequirement, 9 }, 10 }; 11 use serde::Deserialize; 12 use serde_json::Value; 13 use std::{borrow::Cow, collections::BTreeSet, fs, path::Path}; 14 15 const PACKAGED_VECTORS: &str = include_str!("fixtures/bud11_claims.v1.json"); 16 const WORKSPACE_VECTOR_PATH: &str = 17 "../../contracts/conformance/vectors/blossom/bud11_claims.v1.json"; 18 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 19 const SUPPORTED_VECTOR_KINDS: [&str; 14] = [ 20 "blossom.bud11.action.parse.valid", 21 "blossom.bud11.action.parse.invalid", 22 "blossom.bud11.server_domain.parse.valid", 23 "blossom.bud11.server_domain.parse.invalid", 24 "blossom.bud11.content.parse.valid", 25 "blossom.bud11.content.parse.invalid", 26 "blossom.bud11.claim.parse.valid", 27 "blossom.bud11.claim.parse.invalid", 28 "blossom.bud11.validation.new.valid", 29 "blossom.bud11.validation.new.invalid", 30 "blossom.bud11.claim.validate.valid", 31 "blossom.bud11.claim.validate.invalid", 32 "blossom.bud11.authored_upload.valid", 33 "blossom.bud11.authored_upload.invalid", 34 ]; 35 36 #[derive(Debug, Deserialize)] 37 #[serde(deny_unknown_fields)] 38 struct Suite { 39 suite: String, 40 contract_version: String, 41 vectors: Vec<Vector>, 42 } 43 44 #[derive(Debug, Deserialize)] 45 #[serde(deny_unknown_fields)] 46 struct Vector { 47 id: String, 48 kind: String, 49 input: Value, 50 expected: Value, 51 } 52 53 #[test] 54 fn checked_in_bud11_vectors_execute_against_public_api() { 55 let vectors = conformance_vectors(); 56 let suite: Suite = serde_json::from_str(&vectors).expect("BUD-11 vectors must parse"); 57 assert_eq!(suite.suite, "blossom_bud11_claims"); 58 assert_eq!(suite.contract_version, "1.0.0"); 59 assert!(!suite.vectors.is_empty()); 60 assert_vector_inventory(&suite.vectors); 61 62 for vector in &suite.vectors { 63 execute(vector); 64 } 65 } 66 67 fn assert_vector_inventory(vectors: &[Vector]) { 68 let mut ids = BTreeSet::new(); 69 let mut kinds = BTreeSet::new(); 70 for vector in vectors { 71 assert!(!vector.id.trim().is_empty(), "vector id must be nonblank"); 72 assert!( 73 ids.insert(vector.id.as_str()), 74 "duplicate vector id {}", 75 vector.id 76 ); 77 assert!( 78 vector.input.is_object(), 79 "{} input must be an object", 80 vector.id 81 ); 82 assert!( 83 vector.expected.is_object(), 84 "{} expected must be an object", 85 vector.id 86 ); 87 kinds.insert(vector.kind.as_str()); 88 } 89 assert_eq!(kinds, BTreeSet::from(SUPPORTED_VECTOR_KINDS)); 90 } 91 92 fn conformance_vectors() -> Cow<'static, str> { 93 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 94 match fs::read_to_string(&workspace_path) { 95 Ok(canonical) => { 96 assert_eq!( 97 canonical, 98 PACKAGED_VECTORS, 99 "packaged BUD-11 vectors must match {}", 100 workspace_path.display() 101 ); 102 Cow::Owned(canonical) 103 } 104 Err(error) 105 if error.kind() == std::io::ErrorKind::NotFound 106 && !Path::new(env!("CARGO_MANIFEST_DIR")) 107 .join(WORKSPACE_CONTRACT_MARKER_PATH) 108 .is_file() => 109 { 110 Cow::Borrowed(PACKAGED_VECTORS) 111 } 112 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 113 } 114 } 115 116 fn execute(vector: &Vector) { 117 match vector.kind.as_str() { 118 "blossom.bud11.action.parse.valid" => action_parse_valid(vector), 119 "blossom.bud11.action.parse.invalid" => action_parse_invalid(vector), 120 "blossom.bud11.server_domain.parse.valid" => server_domain_parse_valid(vector), 121 "blossom.bud11.server_domain.parse.invalid" => server_domain_parse_invalid(vector), 122 "blossom.bud11.content.parse.valid" => content_parse_valid(vector), 123 "blossom.bud11.content.parse.invalid" => content_parse_invalid(vector), 124 "blossom.bud11.claim.parse.valid" => claim_parse_valid(vector), 125 "blossom.bud11.claim.parse.invalid" => claim_parse_invalid(vector), 126 "blossom.bud11.validation.new.valid" => validation_new_valid(vector), 127 "blossom.bud11.validation.new.invalid" => validation_new_invalid(vector), 128 "blossom.bud11.claim.validate.valid" => claim_validate_valid(vector), 129 "blossom.bud11.claim.validate.invalid" => claim_validate_invalid(vector), 130 "blossom.bud11.authored_upload.valid" => authored_upload_valid(vector), 131 "blossom.bud11.authored_upload.invalid" => authored_upload_invalid(vector), 132 kind => panic!("{} uses unsupported vector kind {kind}", vector.id), 133 } 134 } 135 136 fn action_parse_valid(vector: &Vector) { 137 let action = AuthorizationAction::parse(input_str(vector, "action")) 138 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 139 assert_eq!( 140 action.as_str(), 141 expected_str(vector, "action"), 142 "{}", 143 vector.id 144 ); 145 assert_eq!( 146 action.to_string(), 147 expected_str(vector, "action"), 148 "{}", 149 vector.id 150 ); 151 } 152 153 fn action_parse_invalid(vector: &Vector) { 154 let error = AuthorizationAction::parse(input_str(vector, "action")) 155 .expect_err("invalid action vector must fail"); 156 assert_error(vector, error.code()); 157 } 158 159 fn server_domain_parse_valid(vector: &Vector) { 160 let domain = ServerDomain::parse(input_str(vector, "domain")) 161 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 162 assert_eq!( 163 domain.as_str(), 164 expected_str(vector, "domain"), 165 "{}", 166 vector.id 167 ); 168 assert_eq!( 169 domain.to_string(), 170 expected_str(vector, "domain"), 171 "{}", 172 vector.id 173 ); 174 } 175 176 fn server_domain_parse_invalid(vector: &Vector) { 177 let error = ServerDomain::parse(input_str(vector, "domain")) 178 .expect_err("invalid server-domain vector must fail"); 179 assert_error(vector, error.code()); 180 } 181 182 fn content_parse_valid(vector: &Vector) { 183 let content = AuthorizationContent::parse(input_str(vector, "content")) 184 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 185 if let Some(expected) = vector.expected.get("content").and_then(Value::as_str) { 186 assert_eq!(content.as_str(), expected, "{}", vector.id); 187 assert_eq!(content.to_string(), expected, "{}", vector.id); 188 } else { 189 let expected_byte_length = expected_u64(vector, "content_byte_length"); 190 assert_eq!( 191 content.as_str().len() as u64, 192 expected_byte_length, 193 "{}", 194 vector.id 195 ); 196 assert_eq!( 197 content.to_string().len() as u64, 198 expected_byte_length, 199 "{}", 200 vector.id 201 ); 202 } 203 } 204 205 fn content_parse_invalid(vector: &Vector) { 206 let error = AuthorizationContent::parse(input_str(vector, "content")) 207 .expect_err("invalid content vector must fail"); 208 assert_error(vector, error.code()); 209 } 210 211 fn claim_parse_valid(vector: &Vector) { 212 let claim = parse_claim(&vector.input, &vector.id); 213 assert_claim(vector, &claim); 214 } 215 216 fn claim_parse_invalid(vector: &Vector) { 217 let error = try_parse_claim(&vector.input).expect_err("invalid claim vector must fail"); 218 assert_error(vector, error.code()); 219 } 220 221 fn validation_new_valid(vector: &Vector) { 222 let validation = 223 build_validation(vector).unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 224 assert_eq!( 225 validation.max_created_age_seconds(), 226 Some(expected_u64(vector, "max_created_age")), 227 "{}", 228 vector.id 229 ); 230 } 231 232 fn validation_new_invalid(vector: &Vector) { 233 let error = build_validation(vector).expect_err("invalid validation-policy vector must fail"); 234 assert_error(vector, error.code()); 235 } 236 237 fn claim_validate_valid(vector: &Vector) { 238 let claim = parse_claim(&vector.input["claim"], &vector.id); 239 let validation = build_validation(vector) 240 .unwrap_or_else(|error| panic!("{} policy failed: {error}", vector.id)); 241 let validated = claim 242 .validate(&validation) 243 .unwrap_or_else(|error| panic!("{} validation failed: {error}", vector.id)); 244 assert_eq!( 245 validated.action().as_str(), 246 expected_str(vector, "action"), 247 "{}", 248 vector.id 249 ); 250 } 251 252 fn claim_validate_invalid(vector: &Vector) { 253 let claim = parse_claim(&vector.input["claim"], &vector.id); 254 let validation = build_validation(vector) 255 .unwrap_or_else(|error| panic!("{} policy failed: {error}", vector.id)); 256 let error = claim 257 .validate(&validation) 258 .expect_err("invalid claim-validation vector must fail"); 259 assert_error(vector, error.code()); 260 } 261 262 fn authored_upload_valid(vector: &Vector) { 263 let authored = 264 authored_upload(vector).unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 265 let wire = authored.wire_parts(); 266 assert_eq!( 267 wire.kind(), 268 RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, 269 "{}", 270 vector.id 271 ); 272 assert_eq!( 273 wire.kind() as u64, 274 expected_u64(vector, "kind"), 275 "{}", 276 vector.id 277 ); 278 assert_eq!( 279 wire.content(), 280 expected_str(vector, "content"), 281 "{}", 282 vector.id 283 ); 284 assert_eq!( 285 wire.created_at(), 286 expected_u64(vector, "created_at"), 287 "{}", 288 vector.id 289 ); 290 assert_eq!( 291 authored.expiration(), 292 expected_u64(vector, "expiration"), 293 "{}", 294 vector.id 295 ); 296 assert_eq!( 297 serde_json::to_value(wire.tags()).expect("wire tags must serialize"), 298 vector.expected["tags"], 299 "{}", 300 vector.id 301 ); 302 } 303 304 fn authored_upload_invalid(vector: &Vector) { 305 let error = authored_upload(vector).expect_err("invalid authored-upload vector must fail"); 306 assert_error(vector, error.code()); 307 } 308 309 fn authored_upload(vector: &Vector) -> Result<AuthoredUploadClaim, radroots_blossom::Error> { 310 let content = AuthorizationContent::parse(input_str(vector, "content"))?; 311 let server = ServerDomain::parse(input_str(vector, "server_domain"))?; 312 let hash = Sha256::from_hex(input_str(vector, "hash"))?; 313 AuthoredUploadClaim::new( 314 content, 315 server, 316 hash, 317 input_u64(vector, "created_at"), 318 input_u64(vector, "lifetime"), 319 ) 320 } 321 322 fn try_parse_claim(input: &Value) -> Result<AuthorizationClaim, radroots_blossom::Error> { 323 let tags: Vec<Vec<String>> = 324 serde_json::from_value(input["tags"].clone()).expect("claim tags must be string arrays"); 325 AuthorizationClaim::parse( 326 value_str(input, "content"), 327 value_u64(input, "created_at"), 328 &tags, 329 ) 330 } 331 332 fn parse_claim(input: &Value, id: &str) -> AuthorizationClaim { 333 try_parse_claim(input).unwrap_or_else(|error| panic!("{id} claim parse failed: {error}")) 334 } 335 336 fn assert_claim(vector: &Vector, claim: &AuthorizationClaim) { 337 assert_eq!( 338 claim.content().as_str(), 339 expected_str(vector, "content"), 340 "{}", 341 vector.id 342 ); 343 assert_eq!( 344 claim.created_at(), 345 expected_u64(vector, "created_at"), 346 "{}", 347 vector.id 348 ); 349 assert_eq!( 350 claim.action().as_str(), 351 expected_str(vector, "action"), 352 "{}", 353 vector.id 354 ); 355 assert_eq!( 356 claim.expiration(), 357 expected_u64(vector, "expiration"), 358 "{}", 359 vector.id 360 ); 361 let servers: Vec<String> = claim 362 .server_domains() 363 .iter() 364 .map(ToString::to_string) 365 .collect(); 366 assert_eq!( 367 servers, 368 expected_strings(vector, "servers"), 369 "{}", 370 vector.id 371 ); 372 let hashes: Vec<String> = claim.hashes().iter().map(ToString::to_string).collect(); 373 assert_eq!(hashes, expected_strings(vector, "hashes"), "{}", vector.id); 374 } 375 376 fn build_validation(vector: &Vector) -> Result<AuthorizationValidation, radroots_blossom::Error> { 377 let target = parse_target(&vector.input["target"]); 378 let server = ServerDomain::parse(input_str(vector, "server_domain"))?; 379 let server_scope = match input_str(vector, "server_scope") { 380 "optional_any_match" => ServerScopeRequirement::OptionalAnyMatch, 381 "required_any_match" => ServerScopeRequirement::RequiredAnyMatch, 382 value => panic!("{} has unsupported server scope {value}", vector.id), 383 }; 384 AuthorizationValidation::new( 385 target, 386 server, 387 server_scope, 388 input_u64(vector, "now"), 389 input_u64(vector, "max_created_age"), 390 ) 391 } 392 393 fn parse_target(input: &Value) -> AuthorizationTarget { 394 let hash = || Sha256::from_hex(value_str(input, "hash")).expect("target hash must be valid"); 395 match value_str(input, "type") { 396 "get_blob" => AuthorizationTarget::GetBlob(hash()), 397 "upload" => AuthorizationTarget::Upload(hash()), 398 "list" => AuthorizationTarget::List, 399 "delete_blob" => AuthorizationTarget::DeleteBlob(hash()), 400 "mirror" => AuthorizationTarget::Mirror(hash()), 401 "media" => AuthorizationTarget::Media(hash()), 402 value => panic!("unsupported BUD-11 target {value}"), 403 } 404 } 405 406 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 407 value_str(&vector.input, field) 408 } 409 410 fn input_u64(vector: &Vector, field: &str) -> u64 { 411 value_u64(&vector.input, field) 412 } 413 414 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 415 value_str(&vector.expected, field) 416 } 417 418 fn expected_u64(vector: &Vector, field: &str) -> u64 { 419 value_u64(&vector.expected, field) 420 } 421 422 fn expected_strings(vector: &Vector, field: &str) -> Vec<String> { 423 serde_json::from_value(vector.expected[field].clone()) 424 .unwrap_or_else(|error| panic!("{} expected {field} is invalid: {error}", vector.id)) 425 } 426 427 fn value_str<'a>(value: &'a Value, field: &str) -> &'a str { 428 value[field] 429 .as_str() 430 .unwrap_or_else(|| panic!("{field} must be a string")) 431 } 432 433 fn value_u64(value: &Value, field: &str) -> u64 { 434 value[field] 435 .as_u64() 436 .unwrap_or_else(|| panic!("{field} must be an unsigned integer")) 437 } 438 439 fn assert_error(vector: &Vector, actual: &str) { 440 assert_eq!(actual, expected_str(vector, "error"), "{}", vector.id); 441 }