Skip to content

Commit 7f27656

Browse files
authored
Catch new match arm cargo mutants (payjoin#686)
The newest version of cargo mutants v25.0.1 added a new type of mutation that attempts to delete entire match arms. Mutants passing on 7b3ea49 `259 mutants tested in 8m 49s: 163 caught, 96 unviable` Closes payjoin#671
2 parents a854386 + 7b3ea49 commit 7f27656

File tree

4 files changed

+146
-7
lines changed

4 files changed

+146
-7
lines changed

payjoin/src/receive/optional_parameters.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl Params {
105105
(Some(_), None) | (None, Some(_)) => {
106106
warn!("only one additional-fee parameter specified: {params:?}");
107107
}
108-
_ => (),
108+
(None, None) => (),
109109
}
110110

111111
log::debug!("parsed optional parameters: {params:?}");
@@ -134,23 +134,24 @@ impl std::error::Error for Error {
134134

135135
#[cfg(test)]
136136
pub(crate) mod test {
137-
use bitcoin::Amount;
137+
use bitcoin::{Amount, FeeRate};
138138

139139
use super::*;
140140
use crate::receive::optional_parameters::Params;
141141
use crate::Version;
142142

143143
#[test]
144144
fn test_parse_params() {
145-
use bitcoin::FeeRate;
146-
147-
let pairs = url::form_urlencoded::parse(b"maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=1&disableoutputsubstitution=true&optimisticmerge=true");
145+
let pairs = url::form_urlencoded::parse(b"&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true");
148146
let params = Params::from_query_pairs(pairs, &[Version::One])
149147
.expect("Could not parse params from query pairs");
150148
assert_eq!(params.v, Version::One);
151149
assert_eq!(params.output_substitution, OutputSubstitution::Disabled);
152150
assert_eq!(params.additional_fee_contribution, Some((Amount::from_sat(182), 0)));
153-
assert_eq!(params.min_fee_rate, FeeRate::BROADCAST_MIN);
151+
assert_eq!(
152+
params.min_fee_rate,
153+
FeeRate::from_sat_per_vb(2).expect("Could not calculate feerate")
154+
);
154155
#[cfg(feature = "_multiparty")]
155156
assert!(params.optimistic_merge)
156157
}

payjoin/src/uri/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use url::ParseError;
22

33
#[derive(Debug)]
4-
pub struct PjParseError(InternalPjParseError);
4+
pub struct PjParseError(pub(crate) InternalPjParseError);
55

66
#[derive(Debug)]
77
pub(crate) enum InternalPjParseError {

payjoin/src/uri/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,31 @@ mod tests {
315315
);
316316
}
317317

318+
#[test]
319+
fn test_pj_duplicate_params() {
320+
let uri =
321+
"bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pjos=1&pjos=1&pj=HTTPS://EXAMPLE.COM/\
322+
%23OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC";
323+
let pjuri = Uri::try_from(uri);
324+
assert!(matches!(
325+
pjuri,
326+
Err(bitcoin_uri::de::Error::Extras(PjParseError(
327+
InternalPjParseError::DuplicateParams("pjos")
328+
)))
329+
));
330+
let uri =
331+
"bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pjos=1&pj=HTTPS://EXAMPLE.COM/\
332+
%23OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC&pj=HTTPS://EXAMPLE.COM/\
333+
%23OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC";
334+
let pjuri = Uri::try_from(uri);
335+
assert!(matches!(
336+
pjuri,
337+
Err(bitcoin_uri::de::Error::Extras(PjParseError(
338+
InternalPjParseError::DuplicateParams("pj")
339+
)))
340+
));
341+
}
342+
318343
#[test]
319344
fn test_serialize_pjos() {
320345
let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pj=HTTPS://EXAMPLE.COM/%23OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC";

payjoin/tests/integration.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,119 @@ mod integration {
282282
Ok(())
283283
}
284284

285+
#[tokio::test]
286+
async fn test_err_response() -> Result<(), BoxSendSyncError> {
287+
init_tracing();
288+
let mut services = TestServices::initialize().await?;
289+
let result = tokio::select!(
290+
err = services.take_ohttp_relay_handle() => panic!("Ohttp relay exited early: {:?}", err),
291+
err = services.take_directory_handle() => panic!("Directory server exited early: {:?}", err),
292+
res = process_err_res(&services) => res
293+
);
294+
295+
assert!(result.is_ok(), "v2 send receive failed: {:#?}", result.unwrap_err());
296+
297+
async fn process_err_res(services: &TestServices) -> Result<(), BoxError> {
298+
let (_bitcoind, sender, receiver) = init_bitcoind_sender_receiver(None, None)?;
299+
let agent = services.http_agent();
300+
services.wait_for_services_ready().await?;
301+
let directory = services.directory_url();
302+
let ohttp_keys = services.fetch_ohttp_keys().await?;
303+
// **********************
304+
// Inside the Receiver:
305+
let address = receiver.get_new_address(None, None)?.assume_checked();
306+
307+
let new_receiver =
308+
NewReceiver::new(address.clone(), directory.clone(), ohttp_keys.clone(), None)?;
309+
let storage_token =
310+
new_receiver.persist(&mut NoopPersister).map_err(|e| e.to_string())?;
311+
let mut session =
312+
Receiver::load(storage_token, &NoopPersister).map_err(|e| e.to_string())?;
313+
println!("session: {:#?}", &session);
314+
// Poll receive request
315+
let ohttp_relay = services.ohttp_relay_url();
316+
let (req, ctx) = session.extract_req(&ohttp_relay)?;
317+
let response = agent
318+
.post(req.url)
319+
.header("Content-Type", req.content_type)
320+
.body(req.body)
321+
.send()
322+
.await?;
323+
assert!(response.status().is_success(), "error response: {}", response.status());
324+
let response_body =
325+
session.process_res(response.bytes().await?.to_vec().as_slice(), ctx)?;
326+
// No proposal yet since sender has not responded
327+
assert!(response_body.is_none());
328+
329+
// **********************
330+
// Inside the Sender:
331+
// Create a funded PSBT (not broadcasted) to address with amount given in the pj_uri
332+
let pj_uri = Uri::from_str(&session.pj_uri().to_string())
333+
.map_err(|e| e.to_string())?
334+
.assume_checked()
335+
.check_pj_supported()
336+
.map_err(|e| e.to_string())?;
337+
let psbt = build_sweep_psbt(&sender, &pj_uri)?;
338+
let new_sender =
339+
SenderBuilder::new(psbt, pj_uri).build_recommended(FeeRate::BROADCAST_MIN)?;
340+
let storage_token =
341+
new_sender.persist(&mut NoopPersister).map_err(|e| e.to_string())?;
342+
let req_ctx =
343+
Sender::load(storage_token, &NoopPersister).map_err(|e| e.to_string())?;
344+
let (Request { url, body, content_type, .. }, _send_ctx) =
345+
req_ctx.extract_v2(ohttp_relay.to_owned())?;
346+
let response = agent
347+
.post(url.clone())
348+
.header("Content-Type", content_type)
349+
.body(body.clone())
350+
.send()
351+
.await?;
352+
log::info!("Response: {:#?}", &response);
353+
assert!(response.status().is_success(), "error response: {}", response.status());
354+
// POST Original PSBT
355+
356+
// **********************
357+
// Inside the Receiver:
358+
359+
// GET fallback psbt
360+
let (req, ctx) = session.extract_req(&ohttp_relay)?;
361+
let response = agent
362+
.post(req.url)
363+
.header("Content-Type", req.content_type)
364+
.body(req.body)
365+
.send()
366+
.await?;
367+
// POST payjoin
368+
let mut proposal = session
369+
.process_res(response.bytes().await?.to_vec().as_slice(), ctx)?
370+
.expect("proposal should exist");
371+
// Generate replyable error
372+
let server_error = || {
373+
proposal
374+
.clone()
375+
.check_broadcast_suitability(None, |_| Err("mock error".into()))
376+
.expect_err("expected broadcast suitability check to fail")
377+
};
378+
379+
let (err_req, err_ctx) =
380+
proposal.clone().extract_err_req(&server_error().into(), ohttp_relay)?;
381+
let err_response = agent
382+
.post(err_req.url)
383+
.header("Content-Type", err_req.content_type)
384+
.body(err_req.body)
385+
.send()
386+
.await?;
387+
388+
let err_bytes = err_response.bytes().await?;
389+
// Ensure that the error was handled properly
390+
assert!(proposal.process_err_res(&err_bytes, err_ctx).is_ok());
391+
392+
Ok(())
393+
}
394+
395+
Ok(())
396+
}
397+
285398
#[tokio::test]
286399
async fn v2_to_v2() -> Result<(), BoxSendSyncError> {
287400
init_tracing();

0 commit comments

Comments
 (0)