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 }