diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3774d51..f925401 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,5 +17,5 @@ jobs: steps: - uses: actions/checkout@v3 - uses: mozilla-actions/sccache-action@v0.0.3 - - run: cargo clippy --all-features - - run: cargo test --all-features + - run: cargo clippy --all-features --all-targets + - run: cargo test --all-features --all-targets diff --git a/README.md b/README.md index 3a25a5d..4f25bfb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This library works as a toolkit so you can serve and make your LNURL requests wi ## Current support -- [LUD-01](https://github.com/lnurl/luds/blob/luds/01.md): ⚠️ core ⚠️ client ⚠️ server 🆘 tests +- [LUD-01](https://github.com/lnurl/luds/blob/luds/01.md): ✅ core ✅ client ⚠️ server ⚠️ tests - [LUD-02](https://github.com/lnurl/luds/blob/luds/02.md): ⚠️ core ⚠️ client 🆘 server 🆘 tests - [LUD-03](https://github.com/lnurl/luds/blob/luds/03.md): ⚠️ core ⚠️ client 🆘 server 🆘 tests - [LUD-04](https://github.com/lnurl/luds/blob/luds/04.md): 🆘 core 🆘 client 🆘 server 🆘 tests @@ -25,7 +25,7 @@ This library works as a toolkit so you can serve and make your LNURL requests wi - [LUD-14](https://github.com/lnurl/luds/blob/luds/14.md): 🆘 core 🆘 client 🆘 server 🆘 tests - [LUD-15](https://github.com/lnurl/luds/blob/luds/15.md): 🆘 core 🆘 client 🆘 server 🆘 tests - [LUD-16](https://github.com/lnurl/luds/blob/luds/16.md): ⚠️ core ⚠️ client 🆘 server 🆘 tests -- [LUD-17](https://github.com/lnurl/luds/blob/luds/17.md): 🆘 core 🆘 client 🆘 server 🆘 tests +- [LUD-17](https://github.com/lnurl/luds/blob/luds/17.md): ⚠️ core ⚠️ client ⚠️ server ⚠️ tests - [LUD-18](https://github.com/lnurl/luds/blob/luds/18.md): 🆘 core 🆘 client 🆘 server 🆘 tests - [LUD-19](https://github.com/lnurl/luds/blob/luds/19.md): 🆘 core 🆘 client 🆘 server 🆘 tests - [LUD-20](https://github.com/lnurl/luds/blob/luds/20.md): ⚠️ core ⚠️ client 🆘 server 🆘 tests diff --git a/src/core.rs b/src/core.rs index 71a8653..ca89c26 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,7 +6,7 @@ pub mod withdrawal_request; /// /// Returns error in case `s` cannot be understood. pub fn resolve(s: &str) -> Result { - if s.starts_with("lnurl1") { + if s.starts_with("lnurl1") | s.starts_with("LNURL1") { resolve_bech32(s) } else if s.starts_with("lnurl") || s.starts_with("keyauth") { resolve_scheme(s) @@ -100,3 +100,56 @@ impl std::str::FromStr for Query { } } } + +#[cfg(test)] +mod tests { + #[test] + fn resolve_bech32() { + let input = "lnurl1dp68gurn8ghj7argv4ex2tnfwvhkumelwv7hqmm0dc6p3ztw"; + assert_eq!( + super::resolve(input).unwrap().to_string(), + "https://there.is/no?s=poon" + ); + + let input = "LNURL1DP68GURN8GHJ7ARGV4EX2TNFWVHKUMELWV7HQMM0DC6P3ZTW"; + assert_eq!( + super::resolve(input).unwrap().to_string(), + "https://there.is/no?s=poon" + ); + } + + #[test] + fn resolve_address() { + assert_eq!( + super::resolve("no-spoon@there.is").unwrap().to_string(), + "https://there.is/.well-known/lnurlp/no-spoon" + ); + } + + #[test] + fn resolve_schemes() { + let input = "lnurlc://there.is/no?s=poon"; + assert_eq!( + super::resolve(input).unwrap().to_string(), + "https://there.is/no?s=poon" + ); + + let input = "lnurlw://there.is/no?s=poon"; + assert_eq!( + super::resolve(input).unwrap().to_string(), + "https://there.is/no?s=poon" + ); + + let input = "lnurlp://there.is/no?s=poon"; + assert_eq!( + super::resolve(input).unwrap().to_string(), + "https://there.is/no?s=poon" + ); + + let input = "keyauth://there.is/no?s=poon"; + assert_eq!( + super::resolve(input).unwrap().to_string(), + "https://there.is/no?s=poon" + ); + } +} diff --git a/src/core/channel_request.rs b/src/core/channel_request.rs index 82714f8..1026c05 100644 --- a/src/core/channel_request.rs +++ b/src/core/channel_request.rs @@ -2,7 +2,7 @@ pub const TAG: &str = "channelRequest"; #[derive(Clone, Debug)] pub struct ChannelRequest { - callback: crate::serde::Url, + callback: url::Url, pub uri: String, k1: String, } @@ -13,13 +13,13 @@ impl ChannelRequest { /// Returns errors on network or deserialization failures. #[must_use] pub fn callback_accept(mut self, remoteid: &str, private: bool) -> url::Url { - self.callback.0.query_pairs_mut().extend_pairs([ + self.callback.query_pairs_mut().extend_pairs([ ("k1", &self.k1 as &str), ("remoteid", remoteid), ("private", if private { "1" } else { "0" }), ]); - self.callback.0 + self.callback } /// # Errors @@ -27,13 +27,13 @@ impl ChannelRequest { /// Returns errors on network or deserialization failures. #[must_use] pub fn callback_cancel(mut self, remoteid: &str) -> url::Url { - self.callback.0.query_pairs_mut().extend_pairs([ + self.callback.query_pairs_mut().extend_pairs([ ("k1", &self.k1 as &str), ("remoteid", remoteid), ("cancel", "1"), ]); - self.callback.0 + self.callback } } @@ -45,7 +45,7 @@ impl std::str::FromStr for ChannelRequest { miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; Ok(ChannelRequest { - callback: d.callback, + callback: d.callback.0, uri: d.uri, k1: d.k1, }) @@ -63,3 +63,68 @@ mod serde { pub k1: String, } } + +#[cfg(test)] +mod tests { + #[test] + fn parse() { + let input = r#" + { + "uri": "noh@ipe:porta", + "callback": "https://yuri?o=callback", + "k1": "caum" + } + "#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); + assert_eq!(parsed.uri, "noh@ipe:porta"); + assert_eq!(parsed.k1, "caum"); + } + + #[test] + fn callback_accept() { + let input = r#" + { + "uri": "noh@ipe:porta", + "callback": "https://yuri?o=callback", + "k1": "caum" + } + "#; + + let parsed = input.parse::().expect("parse"); + let url = parsed.clone().callback_accept("idremoto", true); + + assert_eq!( + url.to_string(), + "https://yuri/?o=callback&k1=caum&remoteid=idremoto&private=1" + ); + + let url = parsed.callback_accept("idremoto", false); + + assert_eq!( + url.to_string(), + "https://yuri/?o=callback&k1=caum&remoteid=idremoto&private=0" + ); + } + + #[test] + fn callback_cancel() { + let input = r#" + { + "uri": "noh@ipe:porta", + "callback": "https://yuri?o=callback", + "k1": "caum" + } + "#; + + let parsed = input.parse::().expect("parse"); + let url = parsed.callback_cancel("idremoto"); + + assert_eq!( + url.to_string(), + "https://yuri/?o=callback&k1=caum&remoteid=idremoto&cancel=1" + ); + } +} diff --git a/src/core/pay_request.rs b/src/core/pay_request.rs index d640e41..cdd8974 100644 --- a/src/core/pay_request.rs +++ b/src/core/pay_request.rs @@ -226,3 +226,150 @@ mod de { pub disposable: Option, } } + +#[cfg(test)] +mod tests { + #[test] + fn parse_base() { + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "maxSendable": 315, + "minSendable": 314 + } + "#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); + assert_eq!(parsed.short_description, "boneco do steve magal"); + assert_eq!(parsed.min, 314); + assert_eq!(parsed.max, 315); + + assert_eq!(parsed.comment_size, 0); + assert!(parsed.long_description.is_none()); + assert!(parsed.success_action.is_none()); + assert!(parsed.jpeg.is_none()); + assert!(parsed.png.is_none()); + } + + #[test] + fn parse_comment_size() { + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "commentAllowed": 140, + "maxSendable": 315, + "minSendable": 314 + } + "#; + + let parsed = input.parse::().expect("parse"); + assert_eq!(parsed.comment_size, 140); + } + + #[test] + fn parse_long_description() { + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/long-desc\", \"mochila a jato brutal incluida\"]]", + "maxSendable": 315, + "minSendable": 314 + } + "#; + + let parsed = input.parse::().expect("parse"); + assert_eq!( + parsed.long_description.unwrap(), + "mochila a jato brutal incluida" + ); + } + + #[test] + fn parse_images() { + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"image/png;base64\", \"Zm90b2JydXRhbA==\"],[\"image/jpeg;base64\", \"aW1hZ2VtYnJ1dGFs\"]]", + "maxSendable": 315, + "minSendable": 314 + } + "#; + + let parsed = input.parse::().expect("parse"); + assert_eq!(parsed.jpeg.unwrap(), b"imagembrutal"); + assert_eq!(parsed.png.unwrap(), b"fotobrutal"); + } + + #[test] + fn parse_success_actions() { + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "maxSendable": 315, + "minSendable": 314, + "successAction": { + "tag": "message", + "message": "obrigado!" + } + } + "#; + + let parsed = input.parse::().expect("parse"); + let Some(super::SuccessAction::Message(m)) = parsed.success_action else { + panic!("bad success action"); + }; + + assert_eq!(m, "obrigado!"); + + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "maxSendable": 315, + "minSendable": 314, + "successAction": { + "tag": "url", + "description": "valeu demais", + "url": "http://uerre.ele" + } + } + "#; + + let parsed = input.parse::().expect("parse"); + let Some(super::SuccessAction::Url(u, d)) = parsed.success_action else { + panic!("bad success action"); + }; + + assert_eq!(u.to_string(), "http://uerre.ele/"); + assert_eq!(d, "valeu demais"); + } + + #[test] + fn callback() { + let input = r#" + { + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "maxSendable": 315, + "minSendable": 314 + } + "#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!( + parsed.clone().callback("comentario", 314).to_string(), + "https://yuri/?o=callback&comment=comentario&amount=314" + ); + + assert_eq!( + parsed.callback("", 314).to_string(), + "https://yuri/?o=callback&amount=314" + ); + } +} diff --git a/src/core/withdrawal_request.rs b/src/core/withdrawal_request.rs index 016c7f8..da1d392 100644 --- a/src/core/withdrawal_request.rs +++ b/src/core/withdrawal_request.rs @@ -3,7 +3,7 @@ pub const TAG: &str = "withdrawalRequest"; #[derive(Clone, Debug)] pub struct WithdrawalRequest { k1: String, - callback: crate::serde::Url, + callback: url::Url, pub description: String, pub min: u64, pub max: u64, @@ -18,7 +18,7 @@ impl std::str::FromStr for WithdrawalRequest { Ok(WithdrawalRequest { k1: d.k1, - callback: d.callback, + callback: d.callback.0, description: d.default_description, min: d.min_withdrawable, max: d.max_withdrawable, @@ -33,11 +33,10 @@ impl WithdrawalRequest { #[must_use] pub fn callback(mut self, pr: &str) -> url::Url { self.callback - .0 .query_pairs_mut() .extend_pairs([("k1", &self.k1 as &str), ("pr", pr)]); - self.callback.0 + self.callback } } @@ -57,3 +56,47 @@ mod serde { pub max_withdrawable: u64, } } + +#[cfg(test)] +mod tests { + #[test] + fn parse() { + let input = r#" + { + "k1": "caum", + "callback": "https://yuri?o=callback", + "defaultDescription": "verde com bolinhas", + "minWithdrawable": 314, + "maxWithdrawable": 315 + } + "#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); + assert_eq!(parsed.description, "verde com bolinhas"); + assert_eq!(parsed.k1, "caum"); + assert_eq!(parsed.max, 315); + assert_eq!(parsed.min, 314); + } + + #[test] + fn callback() { + let input = r#" + { + "k1": "caum", + "callback": "https://yuri?o=callback", + "defaultDescription": "verde com bolinhas", + "minWithdrawable": 314, + "maxWithdrawable": 315 + } + "#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!( + parsed.callback("pierre").to_string(), + "https://yuri/?o=callback&k1=caum&pr=pierre" + ); + } +}