job_util.rs (10250B)
1 #[path = "../src/test_fixtures.rs"] 2 mod test_fixtures; 3 4 use radroots_events::job::{JobFeedbackStatus, JobInputType}; 5 use radroots_events_codec::job::error::JobParseError; 6 use radroots_events_codec::job::util::{ 7 feedback_status_from_tag, feedback_status_tag, job_input_type_from_tag, job_input_type_tag, 8 parse_amount_tag_sat, parse_bid_tag_sat, parse_bool_encrypted, parse_i_tags, parse_params, 9 push_amount_tag_msat, push_bid_tag_sat, 10 }; 11 use test_fixtures::{APP_PRIMARY_HTTPS, RELAY_PRIMARY_WSS}; 12 13 #[test] 14 fn parse_bool_encrypted_detects_tag() { 15 let tags = vec![vec!["encrypted".to_string()]]; 16 assert!(parse_bool_encrypted(&tags)); 17 assert!(!parse_bool_encrypted(&[])); 18 } 19 20 #[test] 21 fn input_type_tag_roundtrip() { 22 let t = job_input_type_tag(JobInputType::Url); 23 assert_eq!(job_input_type_from_tag(t), Some(JobInputType::Url)); 24 assert_eq!(job_input_type_from_tag("unknown"), None); 25 } 26 27 #[test] 28 fn input_type_tag_covers_all_variants() { 29 assert_eq!(job_input_type_tag(JobInputType::Event), "event"); 30 assert_eq!(job_input_type_tag(JobInputType::Job), "job"); 31 assert_eq!(job_input_type_tag(JobInputType::Text), "text"); 32 assert_eq!(job_input_type_from_tag("event"), Some(JobInputType::Event)); 33 assert_eq!(job_input_type_from_tag("job"), Some(JobInputType::Job)); 34 assert_eq!(job_input_type_from_tag("text"), Some(JobInputType::Text)); 35 } 36 37 #[test] 38 fn feedback_status_tag_roundtrip() { 39 let t = feedback_status_tag(JobFeedbackStatus::Processing); 40 assert_eq!( 41 feedback_status_from_tag(t), 42 Some(JobFeedbackStatus::Processing) 43 ); 44 assert_eq!(feedback_status_from_tag("unknown"), None); 45 } 46 47 #[test] 48 fn feedback_status_tag_covers_all_variants() { 49 assert_eq!( 50 feedback_status_tag(JobFeedbackStatus::PaymentRequired), 51 "payment-required" 52 ); 53 assert_eq!(feedback_status_tag(JobFeedbackStatus::Error), "error"); 54 assert_eq!(feedback_status_tag(JobFeedbackStatus::Success), "success"); 55 assert_eq!(feedback_status_tag(JobFeedbackStatus::Partial), "partial"); 56 assert_eq!( 57 feedback_status_from_tag("payment-required"), 58 Some(JobFeedbackStatus::PaymentRequired) 59 ); 60 assert_eq!( 61 feedback_status_from_tag("error"), 62 Some(JobFeedbackStatus::Error) 63 ); 64 assert_eq!( 65 feedback_status_from_tag("success"), 66 Some(JobFeedbackStatus::Success) 67 ); 68 assert_eq!( 69 feedback_status_from_tag("partial"), 70 Some(JobFeedbackStatus::Partial) 71 ); 72 } 73 74 #[test] 75 fn parse_i_tags_handles_multiple_shapes() { 76 let tags = vec![ 77 vec!["i".to_string(), APP_PRIMARY_HTTPS.to_string()], 78 vec!["i".to_string(), "note1abcdef".to_string()], 79 vec![ 80 "i".to_string(), 81 "0123456789abcdef0123456789abcdef".to_string(), 82 ], 83 vec![ 84 "i".to_string(), 85 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), 86 ], 87 vec![ 88 "i".to_string(), 89 "job-id".to_string(), 90 "job".to_string(), 91 "wss://relay".to_string(), 92 "marker".to_string(), 93 ], 94 ]; 95 96 let inputs = parse_i_tags(&tags); 97 assert_eq!(inputs.len(), 5); 98 99 assert_eq!(inputs[0].data, APP_PRIMARY_HTTPS); 100 assert_eq!(inputs[0].input_type, JobInputType::Url); 101 assert!(inputs[0].relay.is_none()); 102 assert!(inputs[0].marker.is_none()); 103 104 assert_eq!(inputs[1].data, "note1abcdef"); 105 assert_eq!(inputs[1].input_type, JobInputType::Event); 106 107 assert_eq!(inputs[2].data, "0123456789abcdef0123456789abcdef"); 108 assert_eq!(inputs[2].input_type, JobInputType::Event); 109 assert!(inputs[2].relay.is_none()); 110 assert!(inputs[2].marker.is_none()); 111 112 assert_eq!( 113 inputs[3].data, 114 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 115 ); 116 assert_eq!(inputs[3].input_type, JobInputType::Event); 117 assert!(inputs[3].relay.is_none()); 118 assert!(inputs[3].marker.is_none()); 119 120 assert_eq!(inputs[4].data, "job-id"); 121 assert_eq!(inputs[4].input_type, JobInputType::Job); 122 assert_eq!(inputs[4].relay.as_deref(), Some("wss://relay")); 123 assert_eq!(inputs[4].marker.as_deref(), Some("marker")); 124 } 125 126 #[test] 127 fn parse_i_tags_http_url_uses_url_type() { 128 let tags = vec![vec!["i".to_string(), "http://example.com".to_string()]]; 129 let inputs = parse_i_tags(&tags); 130 assert_eq!(inputs.len(), 1); 131 assert_eq!(inputs[0].input_type, JobInputType::Url); 132 assert_eq!(inputs[0].data, "http://example.com"); 133 } 134 135 #[test] 136 fn parse_i_tags_covers_marker_and_fallback_shapes() { 137 let tags = vec![ 138 vec!["i".to_string()], 139 vec!["i".to_string(), "marker-only".to_string()], 140 vec![ 141 "i".to_string(), 142 "event-id".to_string(), 143 "marker".to_string(), 144 ], 145 vec![ 146 "i".to_string(), 147 "event-id".to_string(), 148 "event".to_string(), 149 "marker-4".to_string(), 150 ], 151 vec![ 152 "i".to_string(), 153 "event-id".to_string(), 154 "event".to_string(), 155 RELAY_PRIMARY_WSS.to_string(), 156 ], 157 vec![ 158 "i".to_string(), 159 "event-id".to_string(), 160 "event".to_string(), 161 "marker-5".to_string(), 162 "fallback-marker".to_string(), 163 ], 164 vec![ 165 "i".to_string(), 166 "event-id".to_string(), 167 "event".to_string(), 168 RELAY_PRIMARY_WSS.to_string(), 169 "final-marker".to_string(), 170 ], 171 vec!["i".to_string(), "nostr:note1abcdef".to_string()], 172 vec!["i".to_string(), "nevent1abcdef".to_string()], 173 vec!["i".to_string(), "naddr1abcdef".to_string()], 174 vec![ 175 "i".to_string(), 176 "text-input".to_string(), 177 "text".to_string(), 178 "ws://relay.example.com".to_string(), 179 "marker-text".to_string(), 180 ], 181 ]; 182 183 let inputs = parse_i_tags(&tags); 184 assert_eq!(inputs.len(), 10); 185 assert_eq!(inputs[0].marker.as_deref(), Some("marker-only")); 186 assert_eq!(inputs[0].data, ""); 187 assert_eq!(inputs[1].marker.as_deref(), Some("marker")); 188 assert_eq!(inputs[1].data, "event-id"); 189 assert_eq!(inputs[2].marker.as_deref(), Some("marker-4")); 190 assert_eq!(inputs[2].relay, None); 191 assert_eq!(inputs[3].relay.as_deref(), Some(RELAY_PRIMARY_WSS)); 192 assert_eq!(inputs[3].marker, None); 193 assert_eq!(inputs[4].marker.as_deref(), Some("marker-5")); 194 assert_eq!(inputs[4].relay, None); 195 assert_eq!(inputs[5].relay.as_deref(), Some(RELAY_PRIMARY_WSS)); 196 assert_eq!(inputs[5].marker.as_deref(), Some("final-marker")); 197 assert_eq!(inputs[6].input_type, JobInputType::Event); 198 assert_eq!(inputs[7].input_type, JobInputType::Event); 199 assert_eq!(inputs[8].input_type, JobInputType::Event); 200 assert_eq!(inputs[9].input_type, JobInputType::Text); 201 assert_eq!(inputs[9].relay.as_deref(), Some("ws://relay.example.com")); 202 assert_eq!(inputs[9].marker.as_deref(), Some("marker-text")); 203 } 204 205 #[test] 206 fn parse_params_extracts_key_value_pairs() { 207 let tags = vec![ 208 vec!["param".to_string(), "k".to_string(), "v".to_string()], 209 vec!["param".to_string(), "skip".to_string()], 210 ]; 211 212 let params = parse_params(&tags); 213 assert_eq!(params.len(), 1); 214 assert_eq!(params[0].key, "k"); 215 assert_eq!(params[0].value, "v"); 216 } 217 218 #[test] 219 fn parse_amount_tag_sat_accepts_msat_and_bolt11() { 220 let tags = vec![vec![ 221 "amount".to_string(), 222 "1000".to_string(), 223 "bolt11".to_string(), 224 ]]; 225 226 let parsed = parse_amount_tag_sat(&tags).unwrap().unwrap(); 227 assert_eq!(parsed.0, 1); 228 assert_eq!(parsed.1.as_deref(), Some("bolt11")); 229 } 230 231 #[test] 232 fn parse_amount_tag_sat_handles_none_and_invalid_shapes() { 233 assert!(parse_amount_tag_sat(&[]).unwrap().is_none()); 234 235 let err = parse_amount_tag_sat(&[vec!["amount".to_string()]]).unwrap_err(); 236 assert!(matches!(err, JobParseError::InvalidTag("amount"))); 237 238 let err = parse_amount_tag_sat(&[vec!["amount".to_string(), "abc".to_string()]]).unwrap_err(); 239 assert!(matches!(err, JobParseError::InvalidNumber("amount", _))); 240 } 241 242 #[test] 243 fn parse_amount_tag_sat_rejects_non_whole_sats() { 244 let tags = vec![vec!["amount".to_string(), "1500".to_string()]]; 245 let err = parse_amount_tag_sat(&tags).unwrap_err(); 246 assert!(matches!(err, JobParseError::NonWholeSats("amount"))); 247 } 248 249 #[test] 250 fn parse_amount_tag_sat_rejects_overflow() { 251 let overflow = ((u32::MAX as u64) + 1) * 1000; 252 let tags = vec![vec!["amount".to_string(), overflow.to_string()]]; 253 let err = parse_amount_tag_sat(&tags).unwrap_err(); 254 assert!(matches!(err, JobParseError::AmountOverflow("amount"))); 255 } 256 257 #[test] 258 fn push_amount_tag_msat_writes_msat() { 259 let mut tags = Vec::new(); 260 push_amount_tag_msat(&mut tags, 12, Some("bolt".to_string())); 261 assert_eq!( 262 tags[0], 263 vec![ 264 "amount".to_string(), 265 "12000".to_string(), 266 "bolt".to_string() 267 ] 268 ); 269 } 270 271 #[test] 272 fn parse_bid_tag_sat_accepts_sat() { 273 let tags = vec![vec!["bid".to_string(), "2".to_string()]]; 274 let bid = parse_bid_tag_sat(&tags).unwrap().unwrap(); 275 assert_eq!(bid, 2); 276 } 277 278 #[test] 279 fn parse_bid_tag_sat_handles_none_and_invalid_shape() { 280 assert!(parse_bid_tag_sat(&[]).unwrap().is_none()); 281 282 let err = parse_bid_tag_sat(&[vec!["bid".to_string()]]).unwrap_err(); 283 assert!(matches!(err, JobParseError::InvalidTag("bid"))); 284 } 285 286 #[test] 287 fn parse_bid_tag_sat_rejects_non_numeric() { 288 let tags = vec![vec!["bid".to_string(), "not-a-number".to_string()]]; 289 let err = parse_bid_tag_sat(&tags).unwrap_err(); 290 assert!(matches!(err, JobParseError::InvalidNumber("bid", _))); 291 } 292 293 #[test] 294 fn parse_bid_tag_sat_rejects_overflow() { 295 let overflow = (u32::MAX as u64) + 1; 296 let tags = vec![vec!["bid".to_string(), overflow.to_string()]]; 297 let err = parse_bid_tag_sat(&tags).unwrap_err(); 298 assert!(matches!(err, JobParseError::AmountOverflow("bid"))); 299 } 300 301 #[test] 302 fn push_bid_tag_sat_writes_sat() { 303 let mut tags = Vec::new(); 304 push_bid_tag_sat(&mut tags, 7); 305 assert_eq!(tags[0], vec!["bid".to_string(), "7".to_string()]); 306 }