lib

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

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 }