lib

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

job_feedback.rs (7038B)


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