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 }