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