lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }