lib

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

job_result.rs (7981B)


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