this repo has no description
1use tranquil_pds::delegation::{intersect_scopes, scopes::validate_delegation_scopes}; 2use tranquil_pds::oauth::scopes::{ 3 AccountAction, IdentityAttr, ParsedScope, RepoAction, ScopePermissions, parse_scope, 4 parse_scope_string, 5}; 6 7#[test] 8fn test_repo_star_defaults_to_all_actions() { 9 let scope = parse_scope("repo:*"); 10 if let ParsedScope::Repo(repo) = scope { 11 assert!(repo.actions.contains(&RepoAction::Create)); 12 assert!(repo.actions.contains(&RepoAction::Update)); 13 assert!(repo.actions.contains(&RepoAction::Delete)); 14 assert_eq!(repo.actions.len(), 3); 15 } else { 16 panic!("Expected Repo scope"); 17 } 18} 19 20#[test] 21fn test_repo_collection_without_actions_defaults_to_all() { 22 let scope = parse_scope("repo:app.bsky.feed.post"); 23 if let ParsedScope::Repo(repo) = scope { 24 assert!(repo.actions.contains(&RepoAction::Create)); 25 assert!(repo.actions.contains(&RepoAction::Update)); 26 assert!(repo.actions.contains(&RepoAction::Delete)); 27 } else { 28 panic!("Expected Repo scope"); 29 } 30} 31 32#[test] 33fn test_repo_empty_string_after_colon() { 34 let scope = parse_scope("repo:"); 35 if let ParsedScope::Repo(repo) = scope { 36 assert!(repo.collection.is_none()); 37 } else { 38 panic!("Expected Repo scope"); 39 } 40} 41 42#[test] 43fn test_rpc_wildcard_aud_wildcard_forbidden() { 44 let scope = parse_scope("rpc:*?aud=*"); 45 assert!(matches!(scope, ParsedScope::Unknown(_))); 46} 47 48#[test] 49fn test_rpc_no_lxm_aud_wildcard_forbidden() { 50 let scope = parse_scope("rpc?aud=*"); 51 assert!(matches!(scope, ParsedScope::Unknown(_))); 52} 53 54#[test] 55fn test_rpc_specific_lxm_wildcard_aud_allowed() { 56 let scope = parse_scope("rpc:app.bsky.feed.getTimeline?aud=*"); 57 assert!(matches!(scope, ParsedScope::Rpc(_))); 58} 59 60#[test] 61fn test_rpc_wildcard_lxm_specific_aud_allowed() { 62 let scope = parse_scope("rpc:*?aud=did:web:api.bsky.app"); 63 assert!(matches!(scope, ParsedScope::Rpc(_))); 64} 65 66#[test] 67fn test_unknown_scope_preserved() { 68 let scope = parse_scope("completely:made:up:scope"); 69 if let ParsedScope::Unknown(s) = scope { 70 assert_eq!(s, "completely:made:up:scope"); 71 } else { 72 panic!("Expected Unknown scope"); 73 } 74} 75 76#[test] 77fn test_unknown_scope_with_params_preserved() { 78 let scope = parse_scope("unknown:thing?param=value"); 79 if let ParsedScope::Unknown(s) = scope { 80 assert_eq!(s, "unknown:thing?param=value"); 81 } else { 82 panic!("Expected Unknown scope"); 83 } 84} 85 86#[test] 87fn test_blob_empty_accept() { 88 let scope = parse_scope("blob"); 89 if let ParsedScope::Blob(blob) = scope { 90 assert!(blob.accept.is_empty()); 91 assert!(blob.matches_mime("anything/goes")); 92 } else { 93 panic!("Expected Blob scope"); 94 } 95} 96 97#[test] 98fn test_blob_matches_wildcard() { 99 let scope = parse_scope("blob:*/*"); 100 if let ParsedScope::Blob(blob) = scope { 101 assert!(blob.matches_mime("image/png")); 102 assert!(blob.matches_mime("video/mp4")); 103 assert!(blob.matches_mime("application/json")); 104 } else { 105 panic!("Expected Blob scope"); 106 } 107} 108 109#[test] 110fn test_blob_type_prefix_matching() { 111 let scope = parse_scope("blob:image/*"); 112 if let ParsedScope::Blob(blob) = scope { 113 assert!(blob.matches_mime("image/png")); 114 assert!(blob.matches_mime("image/jpeg")); 115 assert!(blob.matches_mime("image/gif")); 116 assert!(!blob.matches_mime("video/mp4")); 117 assert!(!blob.matches_mime("images/png")); 118 } else { 119 panic!("Expected Blob scope"); 120 } 121} 122 123#[test] 124fn test_account_default_action_is_read() { 125 let scope = parse_scope("account:email"); 126 if let ParsedScope::Account(a) = scope { 127 assert_eq!(a.action, AccountAction::Read); 128 } else { 129 panic!("Expected Account scope"); 130 } 131} 132 133#[test] 134fn test_multiple_scopes_parsing() { 135 let scopes = parse_scope_string("atproto repo:* blob:*/* transition:generic"); 136 assert_eq!(scopes.len(), 4); 137 assert!(matches!(scopes[0], ParsedScope::Atproto)); 138} 139 140#[test] 141fn test_permissions_null_scope_defaults_atproto() { 142 let perms = ScopePermissions::from_scope_string(None); 143 assert!(perms.has_full_access()); 144 assert!(perms.allows_repo(RepoAction::Create, "any.collection")); 145 assert!(perms.allows_repo(RepoAction::Update, "any.collection")); 146 assert!(perms.allows_repo(RepoAction::Delete, "any.collection")); 147} 148 149#[test] 150fn test_permissions_empty_string_defaults_atproto() { 151 let perms = ScopePermissions::from_scope_string(Some("")); 152 assert!(!perms.has_full_access()); 153} 154 155#[test] 156fn test_permissions_whitespace_only() { 157 let perms = ScopePermissions::from_scope_string(Some(" ")); 158 assert!(!perms.has_full_access()); 159} 160 161#[test] 162fn test_permissions_repo_collection_wildcard_prefix() { 163 let perms = ScopePermissions::from_scope_string(Some("repo:app.bsky.*?action=create")); 164 assert!(perms.allows_repo(RepoAction::Create, "app.bsky.feed.post")); 165 assert!(perms.allows_repo(RepoAction::Create, "app.bsky.actor.profile")); 166 assert!(!perms.allows_repo(RepoAction::Create, "com.atproto.repo.blob")); 167 assert!(!perms.allows_repo(RepoAction::Update, "app.bsky.feed.post")); 168} 169 170#[test] 171fn test_permissions_rpc_lxm_wildcard_prefix() { 172 let perms = 173 ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app")); 174 assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 175 assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed")); 176 assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.actor.getProfile")); 177} 178 179#[test] 180fn test_delegation_intersect_params_behavior() { 181 let result = intersect_scopes("repo:*?action=create", "repo:*?action=delete"); 182 183 assert!( 184 result.is_empty() || result.contains("repo:*"), 185 "Delegation intersection with different action params: '{}'", 186 result 187 ); 188} 189 190#[test] 191fn test_delegation_intersect_wildcard_vs_specific() { 192 let result = intersect_scopes("repo:app.bsky.feed.post?action=create", "repo:*"); 193 assert!(result.contains("repo:")); 194} 195 196#[test] 197fn test_delegation_validate_known_prefixes() { 198 assert!(validate_delegation_scopes("atproto").is_ok()); 199 assert!(validate_delegation_scopes("repo:*").is_ok()); 200 assert!(validate_delegation_scopes("blob:*/*").is_ok()); 201 assert!(validate_delegation_scopes("rpc:*").is_ok()); 202 assert!(validate_delegation_scopes("account:email").is_ok()); 203 assert!(validate_delegation_scopes("identity:handle").is_ok()); 204 assert!(validate_delegation_scopes("transition:generic").is_ok()); 205} 206 207#[test] 208fn test_delegation_validate_unknown_prefixes() { 209 assert!(validate_delegation_scopes("invalid:scope").is_err()); 210 assert!(validate_delegation_scopes("custom:something").is_err()); 211 assert!(validate_delegation_scopes("made:up").is_err()); 212} 213 214#[test] 215fn test_delegation_validate_empty() { 216 assert!(validate_delegation_scopes("").is_ok()); 217} 218 219#[test] 220fn test_delegation_validate_multiple() { 221 assert!(validate_delegation_scopes("atproto repo:* blob:*/*").is_ok()); 222 assert!(validate_delegation_scopes("atproto invalid:scope").is_err()); 223} 224 225#[test] 226fn test_delegation_intersect_empty_granted_returns_empty() { 227 assert_eq!(intersect_scopes("atproto", ""), ""); 228 assert_eq!(intersect_scopes("repo:*", ""), ""); 229} 230 231#[test] 232fn test_delegation_intersect_no_overlap() { 233 let result = intersect_scopes("repo:app.bsky.feed.post", "repo:com.atproto.something"); 234 assert!(result.is_empty()); 235} 236 237#[test] 238fn test_scope_with_multiple_params() { 239 let scope = parse_scope("repo:*?action=create&action=delete"); 240 if let ParsedScope::Repo(repo) = scope { 241 assert!(repo.actions.contains(&RepoAction::Create)); 242 assert!(repo.actions.contains(&RepoAction::Delete)); 243 assert!(!repo.actions.contains(&RepoAction::Update)); 244 } else { 245 panic!("Expected Repo scope"); 246 } 247} 248 249#[test] 250fn test_scope_invalid_action_ignored() { 251 let scope = parse_scope("repo:*?action=invalid"); 252 if let ParsedScope::Repo(repo) = scope { 253 assert!(repo.actions.contains(&RepoAction::Create)); 254 assert!(repo.actions.contains(&RepoAction::Update)); 255 assert!(repo.actions.contains(&RepoAction::Delete)); 256 } else { 257 panic!("Expected Repo scope"); 258 } 259} 260 261#[test] 262fn test_include_scope_parsing() { 263 let scope = parse_scope("include:app.bsky.authFullApp?aud=did:web:api.bsky.app"); 264 if let ParsedScope::Include(inc) = scope { 265 assert_eq!(inc.nsid, "app.bsky.authFullApp"); 266 assert_eq!(inc.aud, Some("did:web:api.bsky.app".to_string())); 267 } else { 268 panic!("Expected Include scope"); 269 } 270} 271 272#[test] 273fn test_include_scope_no_aud() { 274 let scope = parse_scope("include:com.example.authBasic"); 275 if let ParsedScope::Include(inc) = scope { 276 assert_eq!(inc.nsid, "com.example.authBasic"); 277 assert!(inc.aud.is_none()); 278 } else { 279 panic!("Expected Include scope"); 280 } 281} 282 283#[test] 284fn test_identity_wildcard_vs_specific() { 285 let wildcard = parse_scope("identity:*"); 286 let specific = parse_scope("identity:handle"); 287 288 assert!(matches!(wildcard, ParsedScope::Identity(i) if i.attr == IdentityAttr::Wildcard)); 289 assert!(matches!(specific, ParsedScope::Identity(i) if i.attr == IdentityAttr::Handle)); 290} 291 292#[test] 293fn test_identity_unknown_attr() { 294 let scope = parse_scope("identity:unknown"); 295 assert!(matches!(scope, ParsedScope::Unknown(_))); 296} 297 298#[test] 299fn test_transition_scopes_exact_match() { 300 assert!(matches!( 301 parse_scope("transition:generic"), 302 ParsedScope::TransitionGeneric 303 )); 304 assert!(matches!( 305 parse_scope("transition:chat.bsky"), 306 ParsedScope::TransitionChat 307 )); 308 assert!(matches!( 309 parse_scope("transition:email"), 310 ParsedScope::TransitionEmail 311 )); 312 assert!(matches!( 313 parse_scope("transition:unknown"), 314 ParsedScope::Unknown(_) 315 )); 316}