lib

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

job_feedback.rs (6696B)


      1 mod common;
      2 
      3 use radroots_events::job::{JobFeedbackStatus, JobPaymentRequest};
      4 use radroots_events::job_feedback::RadrootsJobFeedback;
      5 use radroots_events::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
      6 use radroots_events_codec::job::encode::JobEncodeError;
      7 use radroots_events_codec::job::error::JobParseError;
      8 use radroots_events_codec::job::feedback::decode::{job_feedback_from_tags, parsed_from_event};
      9 use radroots_events_codec::job::feedback::encode::to_wire_parts;
     10 
     11 fn sample_feedback() -> RadrootsJobFeedback {
     12     RadrootsJobFeedback {
     13         kind: KIND_JOB_FEEDBACK as u16,
     14         status: JobFeedbackStatus::Processing,
     15         extra_info: Some("queued".to_string()),
     16         request_event: common::event_ptr("req", Some("wss://relay")),
     17         customer_pubkey: Some("customer".to_string()),
     18         payment: Some(JobPaymentRequest {
     19             amount_sat: 12,
     20             bolt11: None,
     21         }),
     22         content: Some("payload".to_string()),
     23         encrypted: false,
     24     }
     25 }
     26 
     27 #[test]
     28 fn job_feedback_roundtrip_from_tags() {
     29     let fb = sample_feedback();
     30     let content = fb.content.clone().unwrap();
     31     let parts = to_wire_parts(&fb, &content).unwrap();
     32 
     33     let decoded = job_feedback_from_tags(parts.kind, &parts.tags, &content).unwrap();
     34     assert_eq!(decoded, fb);
     35 }
     36 
     37 #[test]
     38 fn job_feedback_from_tags_accepts_e_ref_and_empty_content() {
     39     let tags = vec![
     40         vec![
     41             "e_ref".to_string(),
     42             "req".to_string(),
     43             "wss://relay".to_string(),
     44         ],
     45         vec!["status".to_string(), "processing".to_string()],
     46     ];
     47     let decoded = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "").unwrap();
     48     assert_eq!(decoded.request_event.id, "req");
     49     assert_eq!(decoded.request_event.relays.as_deref(), Some("wss://relay"));
     50     assert!(decoded.content.is_none());
     51 }
     52 
     53 #[test]
     54 fn job_feedback_requires_valid_kind() {
     55     let mut fb = sample_feedback();
     56     fb.kind = KIND_JOB_RESULT_MIN as u16;
     57 
     58     let err = to_wire_parts(&fb, "payload").unwrap_err();
     59     assert!(matches!(
     60         err,
     61         JobEncodeError::InvalidKind(KIND_JOB_RESULT_MIN)
     62     ));
     63 }
     64 
     65 #[test]
     66 fn job_feedback_requires_status_tag() {
     67     let tags = vec![vec!["e".to_string(), "req".to_string()]];
     68     let err = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "payload").unwrap_err();
     69     assert!(matches!(err, JobParseError::MissingTag("status")));
     70 
     71     let tags = vec![vec!["status".to_string(), "processing".to_string()]];
     72     let err = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "payload").unwrap_err();
     73     assert!(matches!(err, JobParseError::MissingTag("e")));
     74 }
     75 
     76 #[test]
     77 fn job_feedback_rejects_unknown_status() {
     78     let tags = vec![
     79         vec!["status".to_string(), "unknown".to_string()],
     80         vec!["e".to_string(), "req".to_string()],
     81     ];
     82     let err = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "payload").unwrap_err();
     83     assert!(matches!(err, JobParseError::InvalidTag("status")));
     84 
     85     let tags = vec![
     86         vec!["status".to_string(), "processing".to_string()],
     87         vec!["e".to_string()],
     88     ];
     89     let err = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "payload").unwrap_err();
     90     assert!(matches!(err, JobParseError::InvalidTag("e")));
     91 
     92     let tags = vec![
     93         vec!["status".to_string(), "processing".to_string()],
     94         vec!["e".to_string(), "req".to_string()],
     95         vec!["amount".to_string(), "not-a-number".to_string()],
     96     ];
     97     let err = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "payload").unwrap_err();
     98     assert!(matches!(err, JobParseError::InvalidNumber("amount", _)));
     99 }
    100 
    101 #[test]
    102 fn job_feedback_data_from_event_success_path() {
    103     let tags = vec![
    104         vec!["status".to_string(), "processing".to_string()],
    105         vec!["e".to_string(), "req".to_string()],
    106         vec!["amount".to_string(), "12000".to_string()],
    107     ];
    108     let data = radroots_events_codec::job::feedback::decode::data_from_event(
    109         "id".to_string(),
    110         "author".to_string(),
    111         1,
    112         KIND_JOB_FEEDBACK,
    113         "payload".to_string(),
    114         tags,
    115     )
    116     .expect("job feedback data");
    117     assert_eq!(data.id, "id");
    118     assert_eq!(data.author, "author");
    119     assert_eq!(data.kind, KIND_JOB_FEEDBACK);
    120     assert_eq!(data.data.request_event.id, "req");
    121     assert_eq!(data.data.payment.as_ref().map(|p| p.amount_sat), Some(12));
    122 }
    123 
    124 #[test]
    125 fn job_feedback_data_from_event_propagates_decode_errors_with_valid_kind() {
    126     let err = radroots_events_codec::job::feedback::decode::data_from_event(
    127         "id".to_string(),
    128         "author".to_string(),
    129         1,
    130         KIND_JOB_FEEDBACK,
    131         "payload".to_string(),
    132         Vec::new(),
    133     )
    134     .unwrap_err();
    135     assert!(matches!(err, JobParseError::MissingTag("e")));
    136 }
    137 
    138 #[test]
    139 fn job_feedback_metadata_rejects_wrong_kind() {
    140     let err = radroots_events_codec::job::feedback::decode::data_from_event(
    141         "id".to_string(),
    142         "author".to_string(),
    143         1,
    144         KIND_JOB_REQUEST_MIN,
    145         "payload".to_string(),
    146         Vec::new(),
    147     )
    148     .unwrap_err();
    149 
    150     assert!(matches!(
    151         err,
    152         JobParseError::InvalidTag("kind (expected 7000)")
    153     ));
    154 }
    155 
    156 #[test]
    157 fn job_feedback_index_from_event_propagates_parse_errors() {
    158     let err = parsed_from_event(
    159         "id".to_string(),
    160         "author".to_string(),
    161         1,
    162         KIND_JOB_REQUEST_MIN,
    163         "payload".to_string(),
    164         Vec::new(),
    165         "sig".to_string(),
    166     )
    167     .unwrap_err();
    168 
    169     assert!(matches!(
    170         err,
    171         JobParseError::InvalidTag("kind (expected 7000)")
    172     ));
    173 }
    174 
    175 #[test]
    176 fn job_feedback_build_tags_cover_optional_paths() {
    177     let mut fb = sample_feedback();
    178     fb.extra_info = None;
    179     fb.payment = None;
    180     fb.request_event.relays = None;
    181     fb.customer_pubkey = None;
    182     fb.encrypted = true;
    183     let parts = to_wire_parts(&fb, "payload").unwrap();
    184 
    185     let status = parts
    186         .tags
    187         .iter()
    188         .find(|tag| tag.first().map(|v| v.as_str()) == Some("status"))
    189         .expect("status tag");
    190     assert_eq!(status.len(), 2);
    191 
    192     let request = parts
    193         .tags
    194         .iter()
    195         .find(|tag| tag.first().map(|v| v.as_str()) == Some("e"))
    196         .expect("request tag");
    197     assert_eq!(request.len(), 2);
    198 
    199     assert!(
    200         !parts
    201             .tags
    202             .iter()
    203             .any(|tag| tag.first().map(|v| v.as_str()) == Some("amount"))
    204     );
    205     assert!(
    206         !parts
    207             .tags
    208             .iter()
    209             .any(|tag| tag.first().map(|v| v.as_str()) == Some("p"))
    210     );
    211     assert!(
    212         parts
    213             .tags
    214             .iter()
    215             .any(|tag| tag.first().map(|v| v.as_str()) == Some("encrypted"))
    216     );
    217 }