proptest_scope.rs 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. //! `test(proptest_scope)` — scope-parser property suite (task 42).
  2. //!
  3. //! Pure properties over `protocol::scope::ScopeSet::parse`: the parser never
  4. //! panics, rejects only with `invalid_scope`, accepts the RFC 6749
  5. //! scope-token alphabet, enforces the count cap, and refuses empty tokens.
  6. //! Test bodies use `prop_assert!` (no `unwrap`/`expect`/`panic` anywhere).
  7. use d_oauth::{
  8. error::{AppError, OAuthErrorCode},
  9. protocol::scope::{MAX_SCOPE_COUNT, ScopeSet},
  10. };
  11. use proptest::prelude::*;
  12. fn is_invalid_scope<T>(result: Result<T, AppError>) -> bool {
  13. matches!(
  14. result,
  15. Err(AppError::OAuth {
  16. code: OAuthErrorCode::InvalidScope,
  17. ..
  18. })
  19. )
  20. }
  21. proptest! {
  22. #[test]
  23. fn scope_parse_never_panics_and_rejects_only_invalid_scope(raw in "\\PC*") {
  24. match ScopeSet::parse(&raw) {
  25. Ok(set) => {
  26. // An accepted value renders to a form that re-parses identically.
  27. let rendered = set.render();
  28. match ScopeSet::parse(&rendered) {
  29. Ok(again) => {
  30. prop_assert_eq!(set.render(), again.render());
  31. prop_assert!(again.is_subset_of(&set));
  32. }
  33. Err(_) => prop_assert!(false, "rendered form must re-parse"),
  34. }
  35. }
  36. Err(error) => prop_assert!(
  37. matches!(
  38. error,
  39. AppError::OAuth {
  40. code: OAuthErrorCode::InvalidScope,
  41. ..
  42. }
  43. ),
  44. "parser must reject only with invalid_scope"
  45. ),
  46. }
  47. }
  48. #[test]
  49. fn scope_parse_accepts_the_token_alphabet(words in proptest::collection::vec("[!-~&&[^\"\\\\]]{1,16}", 1..8)) {
  50. // `%x21 / %x23-5B / %x5D-7E`: printable ASCII except space, `"`, `\`.
  51. let raw = words.join(" ");
  52. match ScopeSet::parse(&raw) {
  53. Ok(set) => {
  54. prop_assert_eq!(set.render(), {
  55. let mut sorted = words.clone();
  56. sorted.sort();
  57. sorted.dedup();
  58. sorted.join(" ")
  59. });
  60. }
  61. Err(_) => prop_assert!(false, "alphabet words must parse: {:?}", raw),
  62. }
  63. }
  64. #[test]
  65. fn scope_parse_rejects_empty_tokens(words in proptest::collection::vec("[a-z]{1,8}", 2..6)) {
  66. // Joining with a double space always leaves an empty token.
  67. let doubled = words.join(" ");
  68. prop_assert!(is_invalid_scope(ScopeSet::parse(&doubled)));
  69. // A leading space always leaves an empty token.
  70. let leading = format!(" {}", words.join(" "));
  71. prop_assert!(is_invalid_scope(ScopeSet::parse(&leading)));
  72. }
  73. #[test]
  74. fn scope_parse_enforces_the_count_cap(extra in 1..8_usize) {
  75. let many: Vec<String> = (0..MAX_SCOPE_COUNT + extra).map(|i| format!("s{i}")).collect();
  76. prop_assert!(is_invalid_scope(ScopeSet::from_names(&many)));
  77. }
  78. }