profile_conformance.rs (15002B)
1 #![cfg(feature = "json")] 2 3 use std::{borrow::Cow, fs, path::Path}; 4 5 use radroots_blossom::{BlobDescriptor, ByteVerifiedDescriptor}; 6 use radroots_event::{ 7 media::{AuthoredImage, AuthoredImageError}, 8 profile::{ 9 AuthoredProfile, AuthoredProfileError, Nip05Identifier, 10 RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES, 11 }, 12 }; 13 use radroots_event_codec::decode::profile::{ 14 RadrootsProfileMetadataParseError, parse_inbound_profile_metadata, 15 }; 16 use radroots_event_codec::encode::profile::{ 17 RadrootsAuthoredProfileEncodeError, authored_profile_to_wire_parts, 18 }; 19 use serde::Deserialize; 20 use serde_json::{Map, Value}; 21 22 const PACKAGED_VECTORS: &str = include_str!("fixtures/profile_metadata.v1.json"); 23 const WORKSPACE_VECTOR_PATH: &str = "../../contracts/conformance/vectors/profile/metadata.v1.json"; 24 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 25 26 #[derive(Debug, Deserialize)] 27 struct Suite { 28 suite: String, 29 contract_version: String, 30 vectors: Vec<Vector>, 31 } 32 33 #[derive(Debug, Deserialize)] 34 struct Vector { 35 id: String, 36 kind: String, 37 input: Value, 38 expected: Value, 39 } 40 41 #[derive(Debug, Deserialize)] 42 #[serde(deny_unknown_fields)] 43 struct AuthoredProfileInput { 44 name: String, 45 display_name: Option<String>, 46 about: Option<String>, 47 picture: Option<AuthoredMediaInput>, 48 banner: Option<AuthoredMediaInput>, 49 nip05: Option<String>, 50 bot: Option<bool>, 51 } 52 53 #[derive(Debug, Deserialize)] 54 #[serde(deny_unknown_fields)] 55 struct AuthoredMediaInput { 56 bytes_utf8: String, 57 descriptor: BlobDescriptor, 58 } 59 60 #[test] 61 fn checked_in_profile_vectors_execute_against_strict_and_tolerant_public_apis() { 62 let vectors = conformance_vectors(); 63 let suite: Suite = serde_json::from_str(&vectors).expect("Profile vectors must parse"); 64 assert_eq!(suite.suite, "profile_metadata"); 65 assert_eq!(suite.contract_version, "1.0.0"); 66 assert!(!suite.vectors.is_empty()); 67 68 for vector in &suite.vectors { 69 execute(vector); 70 } 71 } 72 73 fn conformance_vectors() -> Cow<'static, str> { 74 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 75 match fs::read_to_string(&workspace_path) { 76 Ok(canonical) => { 77 assert_eq!( 78 canonical, 79 PACKAGED_VECTORS, 80 "packaged Profile vectors must match {}", 81 workspace_path.display() 82 ); 83 Cow::Owned(canonical) 84 } 85 Err(error) 86 if error.kind() == std::io::ErrorKind::NotFound 87 && !Path::new(env!("CARGO_MANIFEST_DIR")) 88 .join(WORKSPACE_CONTRACT_MARKER_PATH) 89 .is_file() => 90 { 91 Cow::Borrowed(PACKAGED_VECTORS) 92 } 93 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 94 } 95 } 96 97 fn execute(vector: &Vector) { 98 match vector.kind.as_str() { 99 "profile.nip05.parse.valid" => nip05_valid(vector), 100 "profile.nip05.parse.invalid" => nip05_invalid(vector), 101 "profile.authored.new.invalid" => authored_name_invalid(vector), 102 "profile.image.from_verified_descriptor.invalid" => authored_image_invalid(vector), 103 "profile.build_authored_draft.valid" => authored_valid(vector), 104 "profile.build_authored_draft.limit.valid" => authored_limit_valid(vector), 105 "profile.build_authored_draft.invalid" => authored_invalid(vector), 106 "profile.parse_inbound_metadata.valid" => inbound_valid(vector), 107 "profile.parse_inbound_metadata.limit.valid" => inbound_limit_valid(vector), 108 "profile.parse_inbound_metadata.invalid" => inbound_invalid(vector), 109 kind => panic!("{} uses unsupported vector kind {kind}", vector.id), 110 } 111 } 112 113 fn nip05_valid(vector: &Vector) { 114 let identifier = Nip05Identifier::parse(input_str(vector, "identifier")) 115 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 116 assert_eq!( 117 identifier.as_str(), 118 expected_str(vector, "identifier"), 119 "{}", 120 vector.id 121 ); 122 assert_eq!( 123 identifier.local_part(), 124 expected_str(vector, "local_part"), 125 "{}", 126 vector.id 127 ); 128 assert_eq!( 129 identifier.domain(), 130 expected_str(vector, "domain"), 131 "{}", 132 vector.id 133 ); 134 assert_eq!( 135 expected_str(vector, "identity_verification"), 136 "not_performed" 137 ); 138 } 139 140 fn nip05_invalid(vector: &Vector) { 141 let error = Nip05Identifier::parse(input_str(vector, "identifier")) 142 .expect_err("invalid NIP-05 vector must fail"); 143 assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id); 144 } 145 146 fn authored_valid(vector: &Vector) { 147 let profile = authored_profile(&vector.input["profile"], &vector.id); 148 let wire = authored_profile_to_wire_parts(&profile) 149 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 150 let expected = &vector.expected["wire_parts"]; 151 assert_eq!( 152 u64::from(wire.kind), 153 expected["kind"].as_u64().unwrap(), 154 "{}", 155 vector.id 156 ); 157 assert_eq!( 158 wire.content, 159 expected["content"].as_str().unwrap(), 160 "{}", 161 vector.id 162 ); 163 assert_eq!( 164 serde_json::to_value(wire.tags).unwrap(), 165 expected["tags"], 166 "{}", 167 vector.id 168 ); 169 assert_eq!( 170 expected_str(vector, "upload_completion"), 171 "not_attested_by_codec" 172 ); 173 } 174 175 fn authored_name_invalid(vector: &Vector) { 176 let error = AuthoredProfile::new(input_str(vector, "name")) 177 .expect_err("invalid authored Profile name must fail"); 178 assert_eq!(error, AuthoredProfileError::InvalidName); 179 assert_eq!(error.code(), expected_str(vector, "error")); 180 } 181 182 fn authored_limit_valid(vector: &Vector) { 183 let name_bytes = vector.input["generated_name_bytes"] 184 .as_u64() 185 .expect("generated_name_bytes") as usize; 186 let profile = AuthoredProfile::new("a".repeat(name_bytes)).unwrap(); 187 let wire = authored_profile_to_wire_parts(&profile) 188 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 189 assert_eq!( 190 u64::from(wire.kind), 191 vector.expected["kind"].as_u64().unwrap() 192 ); 193 assert_eq!(wire.content.len(), expected_usize(vector, "content_bytes")); 194 assert_eq!( 195 wire.content.len(), 196 RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES 197 ); 198 assert_eq!( 199 serde_json::to_value(wire.tags).unwrap(), 200 vector.expected["tags"] 201 ); 202 assert_eq!( 203 expected_str(vector, "upload_completion"), 204 "not_attested_by_codec" 205 ); 206 } 207 208 fn authored_image_invalid(vector: &Vector) { 209 let input: AuthoredMediaInput = serde_json::from_value(vector.input["media"].clone()) 210 .unwrap_or_else(|error| panic!("{} media fixture failed: {error}", vector.id)); 211 let descriptor = verified_descriptor(&input, &vector.id); 212 let error = AuthoredImage::try_from(descriptor).expect_err("non-image Profile media must fail"); 213 assert_eq!(error, AuthoredImageError::MediaTypeNotImage); 214 assert_eq!(error.code(), expected_str(vector, "error")); 215 } 216 217 fn authored_invalid(vector: &Vector) { 218 let name_bytes = vector.input["generated_name_bytes"] 219 .as_u64() 220 .expect("generated_name_bytes") as usize; 221 let profile = AuthoredProfile::new("a".repeat(name_bytes)).unwrap(); 222 let error = authored_profile_to_wire_parts(&profile) 223 .expect_err("oversized authored Profile metadata must fail"); 224 assert_eq!(error.code(), expected_str(vector, "error")); 225 assert!(!error.to_string().is_empty()); 226 match error { 227 RadrootsAuthoredProfileEncodeError::ContentTooLarge { max, actual } => { 228 assert_eq!(max, expected_usize(vector, "max")); 229 assert_eq!(actual, expected_usize(vector, "actual")); 230 assert_eq!(max, RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES); 231 } 232 _ => panic!("{} returned an unexpected authored error", vector.id), 233 } 234 } 235 236 fn authored_profile(input: &Value, vector_id: &str) -> AuthoredProfile { 237 let input: AuthoredProfileInput = serde_json::from_value(input.clone()) 238 .unwrap_or_else(|error| panic!("{vector_id} authored input failed: {error}")); 239 let mut profile = AuthoredProfile::new(input.name) 240 .unwrap_or_else(|error| panic!("{vector_id} name failed: {error}")); 241 if let Some(value) = input.display_name { 242 profile = profile.with_display_name(value); 243 } 244 if let Some(value) = input.about { 245 profile = profile.with_about(value); 246 } 247 if let Some(value) = input.picture { 248 profile = profile.with_picture(authored_image(&value, vector_id)); 249 } 250 if let Some(value) = input.banner { 251 profile = profile.with_banner(authored_image(&value, vector_id)); 252 } 253 if let Some(value) = input.nip05 { 254 profile = profile.with_nip05( 255 Nip05Identifier::parse(&value) 256 .unwrap_or_else(|error| panic!("{vector_id} NIP-05 failed: {error}")), 257 ); 258 } 259 if let Some(value) = input.bot { 260 profile = profile.with_bot(value); 261 } 262 profile 263 } 264 265 fn authored_image(input: &AuthoredMediaInput, vector_id: &str) -> AuthoredImage { 266 AuthoredImage::try_from(verified_descriptor(input, vector_id)) 267 .unwrap_or_else(|error| panic!("{vector_id} Profile image failed: {error}")) 268 } 269 270 fn verified_descriptor(input: &AuthoredMediaInput, vector_id: &str) -> ByteVerifiedDescriptor { 271 let media_type = input.descriptor.media_type().clone(); 272 input 273 .descriptor 274 .clone() 275 .approve_reference() 276 .unwrap_or_else(|error| panic!("{vector_id} descriptor approval failed: {error}")) 277 .verify_bytes(input.bytes_utf8.as_bytes(), &media_type) 278 .unwrap_or_else(|error| panic!("{vector_id} byte verification failed: {error}")) 279 } 280 281 fn inbound_valid(vector: &Vector) { 282 let content = inbound_content(vector); 283 let metadata = parse_inbound_profile_metadata(&content) 284 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 285 assert_eq!(metadata.raw_content(), content.as_ref(), "{}", vector.id); 286 287 let raw: Value = serde_json::from_str(&content).expect("valid vector JSON"); 288 assert_eq!( 289 serde_json::to_value(metadata.raw_fields()).unwrap(), 290 raw, 291 "{}", 292 vector.id 293 ); 294 assert_eq!( 295 serde_json::to_value(metadata.residual_fields()).unwrap(), 296 vector.expected["residual_fields"], 297 "{}", 298 vector.id 299 ); 300 assert_eq!( 301 projected_metadata(&metadata), 302 vector.expected["projected"], 303 "{}", 304 vector.id 305 ); 306 assert_eq!( 307 metadata.nip05_identity_verification().code(), 308 expected_str(vector, "identity_verification") 309 ); 310 311 if let Some(media) = vector.expected.get("media_verification") { 312 if media.get("picture").is_some() { 313 assert_eq!( 314 metadata.picture().expect("picture").to_string(), 315 metadata.picture().unwrap().as_str() 316 ); 317 assert_eq!(media["picture"], "unverified"); 318 } 319 if media.get("banner").is_some() { 320 assert_eq!( 321 metadata.banner().expect("banner").to_string(), 322 metadata.banner().unwrap().as_str() 323 ); 324 assert_eq!(media["banner"], "unverified"); 325 } 326 } 327 } 328 329 fn inbound_limit_valid(vector: &Vector) { 330 let content = inbound_content(vector); 331 let metadata = parse_inbound_profile_metadata(&content) 332 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 333 assert_eq!(content.len(), expected_usize(vector, "content_bytes")); 334 assert_eq!(content.len(), RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES); 335 assert_eq!(metadata.raw_content(), content.as_ref()); 336 assert_eq!(metadata.name().map(str::len), Some(content.len() - 11)); 337 } 338 339 fn projected_metadata( 340 metadata: &radroots_event_codec::decode::profile::RadrootsInboundProfileMetadata, 341 ) -> Value { 342 let mut projected = Map::new(); 343 insert_string(&mut projected, "name", metadata.name()); 344 insert_string(&mut projected, "display_name", metadata.display_name()); 345 insert_string(&mut projected, "about", metadata.about()); 346 insert_string( 347 &mut projected, 348 "picture", 349 metadata.picture().map(|value| value.as_str()), 350 ); 351 insert_string( 352 &mut projected, 353 "banner", 354 metadata.banner().map(|value| value.as_str()), 355 ); 356 insert_string( 357 &mut projected, 358 "nip05", 359 metadata.nip05().map(|value| value.as_str()), 360 ); 361 if let Some(value) = metadata.bot() { 362 projected.insert("bot".to_string(), Value::Bool(value)); 363 } 364 Value::Object(projected) 365 } 366 367 fn insert_string(projected: &mut Map<String, Value>, key: &str, value: Option<&str>) { 368 if let Some(value) = value { 369 projected.insert(key.to_string(), Value::String(value.to_string())); 370 } 371 } 372 373 fn inbound_invalid(vector: &Vector) { 374 let content = inbound_content(vector); 375 let error = parse_inbound_profile_metadata(&content) 376 .expect_err("invalid inbound Profile vector must fail"); 377 assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id); 378 assert!(!error.to_string().is_empty()); 379 match error { 380 RadrootsProfileMetadataParseError::ContentTooLarge { max, actual } => { 381 assert_eq!(max, expected_usize(vector, "max")); 382 assert_eq!(actual, expected_usize(vector, "actual")); 383 } 384 RadrootsProfileMetadataParseError::DuplicateField(field) => { 385 assert_eq!(field, expected_str(vector, "field"), "{}", vector.id); 386 } 387 RadrootsProfileMetadataParseError::InvalidJson 388 | RadrootsProfileMetadataParseError::RootNotObject => {} 389 _ => panic!("{} returned an unexpected inbound error", vector.id), 390 } 391 } 392 393 fn inbound_content<'a>(vector: &'a Vector) -> Cow<'a, str> { 394 if let Some(content) = vector.input.get("content").and_then(Value::as_str) { 395 return Cow::Borrowed(content); 396 } 397 let bytes = vector.input["generated_content_bytes"] 398 .as_u64() 399 .expect("input.generated_content_bytes") as usize; 400 assert!(bytes >= 11); 401 let value = "a".repeat(bytes - 11); 402 let content = format!(r#"{{"name":"{value}"}}"#); 403 assert_eq!(content.len(), bytes); 404 Cow::Owned(content) 405 } 406 407 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 408 vector.input[field] 409 .as_str() 410 .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id)) 411 } 412 413 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 414 vector.expected[field] 415 .as_str() 416 .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id)) 417 } 418 419 fn expected_usize(vector: &Vector, field: &str) -> usize { 420 vector.expected[field] 421 .as_u64() 422 .unwrap_or_else(|| panic!("{} expected.{field} must be an integer", vector.id)) as usize 423 }