mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-25 19:54:14 +00:00
317 lines
9.8 KiB
Rust
317 lines
9.8 KiB
Rust
use tranquil_pds::delegation::{intersect_scopes, scopes::validate_delegation_scopes};
|
|
use tranquil_pds::oauth::scopes::{
|
|
AccountAction, IdentityAttr, ParsedScope, RepoAction, ScopePermissions, parse_scope,
|
|
parse_scope_string,
|
|
};
|
|
|
|
#[test]
|
|
fn test_repo_star_defaults_to_all_actions() {
|
|
let scope = parse_scope("repo:*");
|
|
if let ParsedScope::Repo(repo) = scope {
|
|
assert!(repo.actions.contains(&RepoAction::Create));
|
|
assert!(repo.actions.contains(&RepoAction::Update));
|
|
assert!(repo.actions.contains(&RepoAction::Delete));
|
|
assert_eq!(repo.actions.len(), 3);
|
|
} else {
|
|
panic!("Expected Repo scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_repo_collection_without_actions_defaults_to_all() {
|
|
let scope = parse_scope("repo:app.bsky.feed.post");
|
|
if let ParsedScope::Repo(repo) = scope {
|
|
assert!(repo.actions.contains(&RepoAction::Create));
|
|
assert!(repo.actions.contains(&RepoAction::Update));
|
|
assert!(repo.actions.contains(&RepoAction::Delete));
|
|
} else {
|
|
panic!("Expected Repo scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_repo_empty_string_after_colon() {
|
|
let scope = parse_scope("repo:");
|
|
if let ParsedScope::Repo(repo) = scope {
|
|
assert!(repo.collection.is_none());
|
|
} else {
|
|
panic!("Expected Repo scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_rpc_wildcard_aud_wildcard_forbidden() {
|
|
let scope = parse_scope("rpc:*?aud=*");
|
|
assert!(matches!(scope, ParsedScope::Unknown(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rpc_no_lxm_aud_wildcard_forbidden() {
|
|
let scope = parse_scope("rpc?aud=*");
|
|
assert!(matches!(scope, ParsedScope::Unknown(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rpc_specific_lxm_wildcard_aud_allowed() {
|
|
let scope = parse_scope("rpc:app.bsky.feed.getTimeline?aud=*");
|
|
assert!(matches!(scope, ParsedScope::Rpc(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rpc_wildcard_lxm_specific_aud_allowed() {
|
|
let scope = parse_scope("rpc:*?aud=did:web:api.bsky.app");
|
|
assert!(matches!(scope, ParsedScope::Rpc(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_scope_preserved() {
|
|
let scope = parse_scope("completely:made:up:scope");
|
|
if let ParsedScope::Unknown(s) = scope {
|
|
assert_eq!(s, "completely:made:up:scope");
|
|
} else {
|
|
panic!("Expected Unknown scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_scope_with_params_preserved() {
|
|
let scope = parse_scope("unknown:thing?param=value");
|
|
if let ParsedScope::Unknown(s) = scope {
|
|
assert_eq!(s, "unknown:thing?param=value");
|
|
} else {
|
|
panic!("Expected Unknown scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_blob_empty_accept() {
|
|
let scope = parse_scope("blob");
|
|
if let ParsedScope::Blob(blob) = scope {
|
|
assert!(blob.accept.is_empty());
|
|
assert!(blob.matches_mime("anything/goes"));
|
|
} else {
|
|
panic!("Expected Blob scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_blob_matches_wildcard() {
|
|
let scope = parse_scope("blob:*/*");
|
|
if let ParsedScope::Blob(blob) = scope {
|
|
assert!(blob.matches_mime("image/png"));
|
|
assert!(blob.matches_mime("video/mp4"));
|
|
assert!(blob.matches_mime("application/json"));
|
|
} else {
|
|
panic!("Expected Blob scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_blob_type_prefix_matching() {
|
|
let scope = parse_scope("blob:image/*");
|
|
if let ParsedScope::Blob(blob) = scope {
|
|
assert!(blob.matches_mime("image/png"));
|
|
assert!(blob.matches_mime("image/jpeg"));
|
|
assert!(blob.matches_mime("image/gif"));
|
|
assert!(!blob.matches_mime("video/mp4"));
|
|
assert!(!blob.matches_mime("images/png"));
|
|
} else {
|
|
panic!("Expected Blob scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_account_default_action_is_read() {
|
|
let scope = parse_scope("account:email");
|
|
if let ParsedScope::Account(a) = scope {
|
|
assert_eq!(a.action, AccountAction::Read);
|
|
} else {
|
|
panic!("Expected Account scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_scopes_parsing() {
|
|
let scopes = parse_scope_string("atproto repo:* blob:*/* transition:generic");
|
|
assert_eq!(scopes.len(), 4);
|
|
assert!(matches!(scopes[0], ParsedScope::Atproto));
|
|
}
|
|
|
|
#[test]
|
|
fn test_permissions_null_scope_defaults_atproto() {
|
|
let perms = ScopePermissions::from_scope_string(None);
|
|
assert!(perms.has_full_access());
|
|
assert!(perms.allows_repo(RepoAction::Create, "any.collection"));
|
|
assert!(perms.allows_repo(RepoAction::Update, "any.collection"));
|
|
assert!(perms.allows_repo(RepoAction::Delete, "any.collection"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_permissions_empty_string_defaults_atproto() {
|
|
let perms = ScopePermissions::from_scope_string(Some(""));
|
|
assert!(!perms.has_full_access());
|
|
}
|
|
|
|
#[test]
|
|
fn test_permissions_whitespace_only() {
|
|
let perms = ScopePermissions::from_scope_string(Some(" "));
|
|
assert!(!perms.has_full_access());
|
|
}
|
|
|
|
#[test]
|
|
fn test_permissions_repo_collection_wildcard_prefix() {
|
|
let perms = ScopePermissions::from_scope_string(Some("repo:app.bsky.*?action=create"));
|
|
assert!(perms.allows_repo(RepoAction::Create, "app.bsky.feed.post"));
|
|
assert!(perms.allows_repo(RepoAction::Create, "app.bsky.actor.profile"));
|
|
assert!(!perms.allows_repo(RepoAction::Create, "com.atproto.repo.blob"));
|
|
assert!(!perms.allows_repo(RepoAction::Update, "app.bsky.feed.post"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_permissions_rpc_lxm_wildcard_prefix() {
|
|
let perms =
|
|
ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app"));
|
|
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
|
|
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed"));
|
|
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.actor.getProfile"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_intersect_params_behavior() {
|
|
let result = intersect_scopes("repo:*?action=create", "repo:*?action=delete");
|
|
|
|
assert!(
|
|
result.is_empty() || result.contains("repo:*"),
|
|
"Delegation intersection with different action params: '{}'",
|
|
result
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_intersect_wildcard_vs_specific() {
|
|
let result = intersect_scopes("repo:app.bsky.feed.post?action=create", "repo:*");
|
|
assert!(result.contains("repo:"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_validate_known_prefixes() {
|
|
assert!(validate_delegation_scopes("atproto").is_ok());
|
|
assert!(validate_delegation_scopes("repo:*").is_ok());
|
|
assert!(validate_delegation_scopes("blob:*/*").is_ok());
|
|
assert!(validate_delegation_scopes("rpc:*").is_ok());
|
|
assert!(validate_delegation_scopes("account:email").is_ok());
|
|
assert!(validate_delegation_scopes("identity:handle").is_ok());
|
|
assert!(validate_delegation_scopes("transition:generic").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_validate_unknown_prefixes() {
|
|
assert!(validate_delegation_scopes("invalid:scope").is_err());
|
|
assert!(validate_delegation_scopes("custom:something").is_err());
|
|
assert!(validate_delegation_scopes("made:up").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_validate_empty() {
|
|
assert!(validate_delegation_scopes("").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_validate_multiple() {
|
|
assert!(validate_delegation_scopes("atproto repo:* blob:*/*").is_ok());
|
|
assert!(validate_delegation_scopes("atproto invalid:scope").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_intersect_empty_granted_returns_empty() {
|
|
assert_eq!(intersect_scopes("atproto", ""), "");
|
|
assert_eq!(intersect_scopes("repo:*", ""), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_delegation_intersect_no_overlap() {
|
|
let result = intersect_scopes("repo:app.bsky.feed.post", "repo:com.atproto.something");
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_scope_with_multiple_params() {
|
|
let scope = parse_scope("repo:*?action=create&action=delete");
|
|
if let ParsedScope::Repo(repo) = scope {
|
|
assert!(repo.actions.contains(&RepoAction::Create));
|
|
assert!(repo.actions.contains(&RepoAction::Delete));
|
|
assert!(!repo.actions.contains(&RepoAction::Update));
|
|
} else {
|
|
panic!("Expected Repo scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_scope_invalid_action_ignored() {
|
|
let scope = parse_scope("repo:*?action=invalid");
|
|
if let ParsedScope::Repo(repo) = scope {
|
|
assert!(repo.actions.contains(&RepoAction::Create));
|
|
assert!(repo.actions.contains(&RepoAction::Update));
|
|
assert!(repo.actions.contains(&RepoAction::Delete));
|
|
} else {
|
|
panic!("Expected Repo scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_include_scope_parsing() {
|
|
let scope = parse_scope("include:app.bsky.authFullApp?aud=did:web:api.bsky.app");
|
|
if let ParsedScope::Include(inc) = scope {
|
|
assert_eq!(inc.nsid, "app.bsky.authFullApp");
|
|
assert_eq!(inc.aud, Some("did:web:api.bsky.app".to_string()));
|
|
} else {
|
|
panic!("Expected Include scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_include_scope_no_aud() {
|
|
let scope = parse_scope("include:com.example.authBasic");
|
|
if let ParsedScope::Include(inc) = scope {
|
|
assert_eq!(inc.nsid, "com.example.authBasic");
|
|
assert!(inc.aud.is_none());
|
|
} else {
|
|
panic!("Expected Include scope");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_identity_wildcard_vs_specific() {
|
|
let wildcard = parse_scope("identity:*");
|
|
let specific = parse_scope("identity:handle");
|
|
|
|
assert!(matches!(wildcard, ParsedScope::Identity(i) if i.attr == IdentityAttr::Wildcard));
|
|
assert!(matches!(specific, ParsedScope::Identity(i) if i.attr == IdentityAttr::Handle));
|
|
}
|
|
|
|
#[test]
|
|
fn test_identity_unknown_attr() {
|
|
let scope = parse_scope("identity:unknown");
|
|
assert!(matches!(scope, ParsedScope::Unknown(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_transition_scopes_exact_match() {
|
|
assert!(matches!(
|
|
parse_scope("transition:generic"),
|
|
ParsedScope::TransitionGeneric
|
|
));
|
|
assert!(matches!(
|
|
parse_scope("transition:chat.bsky"),
|
|
ParsedScope::TransitionChat
|
|
));
|
|
assert!(matches!(
|
|
parse_scope("transition:email"),
|
|
ParsedScope::TransitionEmail
|
|
));
|
|
assert!(matches!(
|
|
parse_scope("transition:unknown"),
|
|
ParsedScope::Unknown(_)
|
|
));
|
|
}
|