lib

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

job_result.rs (7670B)


      1 mod common;
      2 #[path = "../src/test_fixtures.rs"]
      3 mod test_fixtures;
      4 
      5 use radroots_events::job::{JobInputType, JobPaymentRequest};
      6 use radroots_events::job_request::RadrootsJobInput;
      7 use radroots_events::job_result::RadrootsJobResult;
      8 use radroots_events::kinds::{KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
      9 use radroots_events_codec::job::encode::JobEncodeError;
     10 use radroots_events_codec::job::error::JobParseError;
     11 use radroots_events_codec::job::result::decode::{job_result_from_tags, parsed_from_event};
     12 use radroots_events_codec::job::result::encode::to_wire_parts;
     13 use test_fixtures::{APP_PRIMARY_HTTPS, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS};
     14 
     15 fn sample_result() -> RadrootsJobResult {
     16     RadrootsJobResult {
     17         kind: (KIND_JOB_RESULT_MIN + 1) as u16,
     18         request_event: common::event_ptr("req", Some(RELAY_PRIMARY_WSS)),
     19         request_json: Some("{\"foo\":\"bar\"}".to_string()),
     20         inputs: vec![RadrootsJobInput {
     21             data: APP_PRIMARY_HTTPS.to_string(),
     22             input_type: JobInputType::Url,
     23             relay: None,
     24             marker: None,
     25         }],
     26         customer_pubkey: Some("customer".to_string()),
     27         payment: Some(JobPaymentRequest {
     28             amount_sat: 50,
     29             bolt11: Some("bolt".to_string()),
     30         }),
     31         content: Some("payload".to_string()),
     32         encrypted: false,
     33     }
     34 }
     35 
     36 #[test]
     37 fn job_result_roundtrip_from_tags() {
     38     let res = sample_result();
     39     let content = res.content.clone().unwrap();
     40     let parts = to_wire_parts(&res, &content).unwrap();
     41 
     42     let decoded = job_result_from_tags(parts.kind, &parts.tags, &content).unwrap();
     43     assert_eq!(decoded, res);
     44 }
     45 
     46 #[test]
     47 fn job_result_roundtrip_with_empty_content_sets_none() {
     48     let res = sample_result();
     49     let parts = to_wire_parts(&res, "").unwrap();
     50     let decoded = job_result_from_tags(parts.kind, &parts.tags, "").unwrap();
     51     assert!(decoded.content.is_none());
     52 }
     53 
     54 #[test]
     55 fn job_result_roundtrip_preserves_input_relay_and_marker() {
     56     let mut res = sample_result();
     57     res.inputs = vec![RadrootsJobInput {
     58         data: "note1payload".to_string(),
     59         input_type: JobInputType::Event,
     60         relay: Some(RELAY_SECONDARY_WSS.to_string()),
     61         marker: Some("root".to_string()),
     62     }];
     63     let content = res.content.clone().unwrap();
     64     let parts = to_wire_parts(&res, &content).unwrap();
     65     let decoded = job_result_from_tags(parts.kind, &parts.tags, &content).unwrap();
     66     assert_eq!(decoded, res);
     67 }
     68 
     69 #[test]
     70 fn job_result_requires_valid_kind() {
     71     let mut res = sample_result();
     72     res.kind = KIND_JOB_REQUEST_MIN as u16;
     73 
     74     let err = to_wire_parts(&res, "payload").unwrap_err();
     75     assert!(matches!(
     76         err,
     77         JobEncodeError::InvalidKind(KIND_JOB_REQUEST_MIN)
     78     ));
     79 }
     80 
     81 #[test]
     82 fn job_result_encrypted_adds_flag_and_rejects_inputs() {
     83     let mut encrypted = sample_result();
     84     encrypted.encrypted = true;
     85     encrypted.inputs.clear();
     86     let content = encrypted.content.clone().unwrap();
     87     let parts = to_wire_parts(&encrypted, &content).unwrap();
     88     assert!(
     89         parts
     90             .tags
     91             .iter()
     92             .any(|tag| tag.first().map(|v| v.as_str()) == Some("encrypted"))
     93     );
     94     assert!(
     95         !parts
     96             .tags
     97             .iter()
     98             .any(|tag| tag.first().map(|v| v.as_str()) == Some("i"))
     99     );
    100 
    101     let mut invalid = sample_result();
    102     invalid.encrypted = true;
    103     let err = to_wire_parts(&invalid, "payload").unwrap_err();
    104     assert!(matches!(
    105         err,
    106         JobEncodeError::EmptyRequiredField("inputs-when-encrypted")
    107     ));
    108 }
    109 
    110 #[test]
    111 fn job_result_build_tags_supports_minimal_optional_fields() {
    112     let mut res = sample_result();
    113     res.request_json = None;
    114     res.inputs.clear();
    115     res.customer_pubkey = None;
    116     res.payment = None;
    117     let parts = to_wire_parts(&res, "payload").unwrap();
    118     assert!(
    119         parts
    120             .tags
    121             .iter()
    122             .any(|tag| tag.first().map(|v| v.as_str()) == Some("e"))
    123     );
    124     assert!(
    125         !parts
    126             .tags
    127             .iter()
    128             .any(|tag| tag.first().map(|v| v.as_str()) == Some("request"))
    129     );
    130     assert!(
    131         !parts
    132             .tags
    133             .iter()
    134             .any(|tag| tag.first().map(|v| v.as_str()) == Some("i"))
    135     );
    136     assert!(
    137         !parts
    138             .tags
    139             .iter()
    140             .any(|tag| tag.first().map(|v| v.as_str()) == Some("p"))
    141     );
    142     assert!(
    143         !parts
    144             .tags
    145             .iter()
    146             .any(|tag| tag.first().map(|v| v.as_str()) == Some("amount"))
    147     );
    148 }
    149 
    150 #[test]
    151 fn job_result_build_tags_omits_request_relay_when_absent() {
    152     let mut res = sample_result();
    153     res.request_event.relays = None;
    154     let parts = to_wire_parts(&res, "payload").unwrap();
    155     let request = parts
    156         .tags
    157         .iter()
    158         .find(|tag| tag.first().map(|v| v.as_str()) == Some("e"))
    159         .expect("request tag");
    160     assert_eq!(request.len(), 2);
    161 }
    162 
    163 #[test]
    164 fn job_result_requires_request_event_tag() {
    165     let tags = vec![vec!["p".to_string(), "customer".to_string()]];
    166     let err = job_result_from_tags(KIND_JOB_RESULT_MIN + 1, &tags, "payload").unwrap_err();
    167     assert!(matches!(err, JobParseError::MissingTag("e")));
    168 
    169     let tags = vec![
    170         vec!["e".to_string()],
    171         vec!["amount".to_string(), "not-a-number".to_string()],
    172     ];
    173     let err = job_result_from_tags(KIND_JOB_RESULT_MIN + 1, &tags, "payload").unwrap_err();
    174     assert!(matches!(err, JobParseError::InvalidTag("e")));
    175 
    176     let tags = vec![
    177         vec!["e".to_string(), "req".to_string()],
    178         vec!["amount".to_string(), "not-a-number".to_string()],
    179     ];
    180     let err = job_result_from_tags(KIND_JOB_RESULT_MIN + 1, &tags, "payload").unwrap_err();
    181     assert!(matches!(err, JobParseError::InvalidNumber("amount", _)));
    182 }
    183 
    184 #[test]
    185 fn job_result_metadata_rejects_wrong_kind() {
    186     let err = radroots_events_codec::job::result::decode::data_from_event(
    187         "id".to_string(),
    188         "author".to_string(),
    189         1,
    190         KIND_JOB_REQUEST_MIN,
    191         "payload".to_string(),
    192         Vec::new(),
    193     )
    194     .unwrap_err();
    195 
    196     assert!(matches!(
    197         err,
    198         JobParseError::InvalidTag("kind (expected 6000-6999)")
    199     ));
    200 }
    201 
    202 #[test]
    203 fn job_result_data_from_event_success_path() {
    204     let result = sample_result();
    205     let content = result.content.clone().unwrap();
    206     let parts = to_wire_parts(&result, &content).expect("wire parts");
    207     let data = radroots_events_codec::job::result::decode::data_from_event(
    208         "id".to_string(),
    209         "author".to_string(),
    210         1,
    211         parts.kind,
    212         content,
    213         parts.tags,
    214     )
    215     .expect("job result data");
    216     assert_eq!(data.id, "id");
    217     assert_eq!(data.author, "author");
    218     assert_eq!(data.kind, KIND_JOB_RESULT_MIN + 1);
    219     assert_eq!(data.data.request_event.id, "req");
    220 }
    221 
    222 #[test]
    223 fn job_result_data_from_event_propagates_decode_errors_with_valid_kind() {
    224     let err = radroots_events_codec::job::result::decode::data_from_event(
    225         "id".to_string(),
    226         "author".to_string(),
    227         1,
    228         KIND_JOB_RESULT_MIN + 1,
    229         "payload".to_string(),
    230         Vec::new(),
    231     )
    232     .unwrap_err();
    233     assert!(matches!(err, JobParseError::MissingTag("e")));
    234 }
    235 
    236 #[test]
    237 fn job_result_index_from_event_propagates_parse_errors() {
    238     let err = parsed_from_event(
    239         "id".to_string(),
    240         "author".to_string(),
    241         1,
    242         KIND_JOB_REQUEST_MIN,
    243         "payload".to_string(),
    244         Vec::new(),
    245         "sig".to_string(),
    246     )
    247     .unwrap_err();
    248     assert!(matches!(
    249         err,
    250         JobParseError::InvalidTag("kind (expected 6000-6999)")
    251     ));
    252 }