| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- //! `test(proptest_redirect)` — redirect-matcher property suite (task 42).
- //!
- //! Pure properties over `protocol::redirect_repo::match_redirect_uri`
- //! (RFC 3986 §6.2.1 Simple String Comparison + RFC 8252 §7.3 loopback):
- //! exact self-match, empty-set rejection, `Exact` implies membership,
- //! `Loopback` implies a loopback request shape, and `Exact` outranks
- //! `Loopback`. No `unwrap`/`expect`/`panic` anywhere.
- use d_oauth::protocol::redirect_repo::{RedirectDecision, match_redirect_uri};
- use proptest::prelude::*;
- /// Well-formed registered redirect URIs (no userinfo/query/fragment).
- ///
- /// The dots are regex-escaped: an unescaped `.` would generate arbitrary
- /// characters (including `?`/`#`/`@`), producing registrations the matcher
- /// must fail closed on instead of matching.
- fn registered_uri() -> impl Strategy<Value = String> {
- (
- "https://client\\.example\\.test/cb/[a-z0-9]{0,8}",
- "[a-z0-9]{0,6}",
- )
- .prop_map(|(base, tail)| {
- if tail.is_empty() {
- base
- } else {
- format!("{base}/{tail}")
- }
- })
- }
- /// Whether `requested` has the loopback request shape the matcher demands
- /// with the flag off: `http` scheme (any ASCII case), loopback-literal host,
- /// no userinfo, query, or fragment (mirrors `parse_uri` + the §7.3 arm, not
- /// its code). `localhost` is excluded because the flag is off here.
- fn is_loopback_shaped(requested: &str) -> bool {
- let Some((scheme, rest)) = requested.split_once(':') else {
- return false;
- };
- if !scheme.eq_ignore_ascii_case("http") || !rest.starts_with("//") {
- return false;
- }
- let authority = &rest[2..];
- let host = authority.split([':', '/']).next().unwrap_or("");
- if host != "127.0.0.1" && host != "[::1]" {
- return false;
- }
- !requested.contains(['?', '#', '@'])
- }
- proptest! {
- #[test]
- fn redirect_exact_self_match(uri in registered_uri()) {
- prop_assert_eq!(
- match_redirect_uri(std::slice::from_ref(&uri), &uri, false),
- RedirectDecision::Exact
- );
- }
- #[test]
- fn redirect_empty_set_always_rejects(requested in "\\PC*") {
- let empty: Vec<String> = vec![];
- prop_assert_eq!(
- match_redirect_uri(&empty, &requested, false),
- RedirectDecision::Reject
- );
- }
- #[test]
- fn redirect_exact_implies_membership(
- registered in proptest::collection::vec(registered_uri(), 0..4),
- requested in "\\PC*",
- ) {
- if match_redirect_uri(®istered, &requested, false) == RedirectDecision::Exact {
- prop_assert!(registered.iter().any(|candidate| candidate == &requested));
- }
- }
- #[test]
- fn redirect_loopback_implies_loopback_request_shape(
- registered in proptest::collection::vec(registered_uri(), 0..4),
- requested in "\\PC*",
- ) {
- if match_redirect_uri(®istered, &requested, false) == RedirectDecision::Loopback {
- prop_assert!(
- is_loopback_shaped(&requested),
- "loopback match needs a loopback request: {:?}",
- requested
- );
- }
- }
- #[test]
- fn redirect_exact_outranks_loopback(port in 1024..65535_u16, path in "[a-z]{1,8}") {
- let loopback_reg = "http://127.0.0.1/cb".to_owned();
- let exact = format!("https://client.example.test/{path}");
- let requested_loopback = format!("http://127.0.0.1:{port}/cb");
- // The exact registration wins when it matches byte-for-byte, even with
- // a loopback candidate that would also match its own request shape.
- prop_assert_eq!(
- match_redirect_uri(&[loopback_reg.clone(), exact.clone()], &exact, false),
- RedirectDecision::Exact
- );
- // And the loopback arm still fires for the port-flexible request.
- prop_assert_eq!(
- match_redirect_uri(&[loopback_reg, exact], &requested_loopback, false),
- RedirectDecision::Loopback
- );
- }
- #[test]
- fn redirect_localhost_loopback_requires_the_flag(
- reg_port in 1024..65535_u16,
- req_port in 1024..65535_u16,
- path in "[a-z]{1,8}",
- ) {
- let registered = vec![format!("http://localhost:{reg_port}/{path}")];
- let requested = format!("http://localhost:{req_port}/{path}");
- // Flag off: `localhost` is never loopback, even for an identical request.
- prop_assert_eq!(
- match_redirect_uri(®istered, &requested, false),
- RedirectDecision::Reject
- );
- // Flag on: same host+path matches with the port ignored; the same port is
- // still an exact byte-for-byte match.
- let expected = if reg_port == req_port {
- RedirectDecision::Exact
- } else {
- RedirectDecision::Loopback
- };
- prop_assert_eq!(match_redirect_uri(®istered, &requested, true), expected);
- }
- }
|