app: add organization + sharing commands/queries
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
use std::sync::Arc;
|
||||
use application::testing::InMemoryShareRepository;
|
||||
use application::sharing::{GenerateShareLinkCommand, GenerateShareLinkHandler};
|
||||
use domain::entities::{LinkAccessLevel, ScopeType, ShareableType};
|
||||
use domain::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[tokio::test]
|
||||
async fn generates_link() {
|
||||
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||
let handler = GenerateShareLinkHandler::new(share_repo);
|
||||
|
||||
let (scope, link) = handler.execute(GenerateShareLinkCommand {
|
||||
shareable_type: ShareableType::Album,
|
||||
shareable_id: SystemId::new(),
|
||||
access_level: LinkAccessLevel::ViewOnly,
|
||||
created_by: SystemId::new(),
|
||||
expires_at: None,
|
||||
max_uses: None,
|
||||
}).await.unwrap();
|
||||
|
||||
assert_eq!(scope.scope_type, ScopeType::Link);
|
||||
assert!(!link.token.is_empty());
|
||||
assert_eq!(link.access_level, LinkAccessLevel::ViewOnly);
|
||||
assert!(link.expires_at.is_none());
|
||||
assert!(link.max_uses.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generates_link_with_expiry_and_max_uses() {
|
||||
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||
let handler = GenerateShareLinkHandler::new(share_repo);
|
||||
|
||||
let expiry = DateTimeStamp::now();
|
||||
let (_, link) = handler.execute(GenerateShareLinkCommand {
|
||||
shareable_type: ShareableType::Collection,
|
||||
shareable_id: SystemId::new(),
|
||||
access_level: LinkAccessLevel::LimitedSearch,
|
||||
created_by: SystemId::new(),
|
||||
expires_at: Some(expiry),
|
||||
max_uses: Some(10),
|
||||
}).await.unwrap();
|
||||
|
||||
assert!(link.expires_at.is_some());
|
||||
assert_eq!(link.max_uses, Some(10));
|
||||
assert_eq!(link.access_level, LinkAccessLevel::LimitedSearch);
|
||||
}
|
||||
3
crates/application/tests/sharing/commands/mod.rs
Normal file
3
crates/application/tests/sharing/commands/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod share_resource;
|
||||
mod generate_share_link;
|
||||
mod revoke_share;
|
||||
48
crates/application/tests/sharing/commands/revoke_share.rs
Normal file
48
crates/application/tests/sharing/commands/revoke_share.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::sync::Arc;
|
||||
use application::testing::{InMemoryShareRepository, StubEventPublisher};
|
||||
use application::sharing::{
|
||||
GenerateShareLinkCommand, GenerateShareLinkHandler,
|
||||
RevokeShareCommand, RevokeShareHandler,
|
||||
};
|
||||
use domain::entities::{LinkAccessLevel, ShareableType};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[tokio::test]
|
||||
async fn revokes_share() {
|
||||
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||
let event_pub = Arc::new(StubEventPublisher::new());
|
||||
|
||||
// Create a scope first via generate_share_link
|
||||
let gen_handler = GenerateShareLinkHandler::new(share_repo.clone());
|
||||
let (scope, _) = gen_handler.execute(GenerateShareLinkCommand {
|
||||
shareable_type: ShareableType::Album,
|
||||
shareable_id: SystemId::new(),
|
||||
access_level: LinkAccessLevel::ViewOnly,
|
||||
created_by: SystemId::new(),
|
||||
expires_at: None,
|
||||
max_uses: None,
|
||||
}).await.unwrap();
|
||||
|
||||
let handler = RevokeShareHandler::new(share_repo, event_pub.clone());
|
||||
handler.execute(RevokeShareCommand {
|
||||
scope_id: scope.scope_id,
|
||||
revoked_by: SystemId::new(),
|
||||
}).await.unwrap();
|
||||
|
||||
let events = event_pub.published().await;
|
||||
assert_eq!(events.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_nonexistent_scope() {
|
||||
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||
let event_pub = Arc::new(StubEventPublisher::new());
|
||||
let handler = RevokeShareHandler::new(share_repo, event_pub);
|
||||
|
||||
let result = handler.execute(RevokeShareCommand {
|
||||
scope_id: SystemId::new(),
|
||||
revoked_by: SystemId::new(),
|
||||
}).await;
|
||||
assert!(matches!(result, Err(DomainError::NotFound(_))));
|
||||
}
|
||||
47
crates/application/tests/sharing/commands/share_resource.rs
Normal file
47
crates/application/tests/sharing/commands/share_resource.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::sync::Arc;
|
||||
use application::testing::{InMemoryShareRepository, StubEventPublisher};
|
||||
use application::sharing::{ShareResourceCommand, ShareResourceHandler};
|
||||
use domain::entities::{ScopeType, ShareableType, TargetType};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[tokio::test]
|
||||
async fn shares_with_user() {
|
||||
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||
let event_pub = Arc::new(StubEventPublisher::new());
|
||||
|
||||
let handler = ShareResourceHandler::new(share_repo, event_pub.clone());
|
||||
let (scope, target) = handler.execute(ShareResourceCommand {
|
||||
shareable_type: ShareableType::Album,
|
||||
shareable_id: SystemId::new(),
|
||||
target_type: TargetType::User,
|
||||
target_id: SystemId::new(),
|
||||
role_id: SystemId::new(),
|
||||
created_by: SystemId::new(),
|
||||
}).await.unwrap();
|
||||
|
||||
assert_eq!(scope.scope_type, ScopeType::User);
|
||||
assert_eq!(target.target_type, TargetType::User);
|
||||
assert_eq!(target.scope_id, scope.scope_id);
|
||||
|
||||
let events = event_pub.published().await;
|
||||
assert_eq!(events.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shares_with_group() {
|
||||
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||
let event_pub = Arc::new(StubEventPublisher::new());
|
||||
|
||||
let handler = ShareResourceHandler::new(share_repo, event_pub.clone());
|
||||
let (scope, target) = handler.execute(ShareResourceCommand {
|
||||
shareable_type: ShareableType::Asset,
|
||||
shareable_id: SystemId::new(),
|
||||
target_type: TargetType::Group,
|
||||
target_id: SystemId::new(),
|
||||
role_id: SystemId::new(),
|
||||
created_by: SystemId::new(),
|
||||
}).await.unwrap();
|
||||
|
||||
assert_eq!(scope.scope_type, ScopeType::Group);
|
||||
assert_eq!(target.target_type, TargetType::Group);
|
||||
}
|
||||
2
crates/application/tests/sharing/mod.rs
Normal file
2
crates/application/tests/sharing/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod commands;
|
||||
mod queries;
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::sync::Arc;
|
||||
use application::testing::InMemoryShareRepository;
|
||||
use application::sharing::{
|
||||
AccessSharedResourceQuery, AccessSharedResourceHandler,
|
||||
GenerateShareLinkCommand, GenerateShareLinkHandler,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use domain::entities::{LinkAccessLevel, ShareableType};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
async fn create_link(
|
||||
repo: &Arc<InMemoryShareRepository>,
|
||||
expires_at: Option<DateTimeStamp>,
|
||||
max_uses: Option<u32>,
|
||||
) -> String {
|
||||
let handler = GenerateShareLinkHandler::new(repo.clone());
|
||||
let (_, link) = handler.execute(GenerateShareLinkCommand {
|
||||
shareable_type: ShareableType::Album,
|
||||
shareable_id: SystemId::new(),
|
||||
access_level: LinkAccessLevel::ViewOnly,
|
||||
created_by: SystemId::new(),
|
||||
expires_at,
|
||||
max_uses,
|
||||
}).await.unwrap();
|
||||
link.token
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_link_returns_scope() {
|
||||
let repo = Arc::new(InMemoryShareRepository::new());
|
||||
let token = create_link(&repo, None, None).await;
|
||||
|
||||
let handler = AccessSharedResourceHandler::new(repo);
|
||||
let (scope, access_level) = handler.execute(AccessSharedResourceQuery {
|
||||
token,
|
||||
}).await.unwrap();
|
||||
|
||||
assert_eq!(access_level, LinkAccessLevel::ViewOnly);
|
||||
assert_eq!(scope.shareable_type, ShareableType::Album);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expired_link_rejected() {
|
||||
let repo = Arc::new(InMemoryShareRepository::new());
|
||||
// Create link with past expiry
|
||||
let past = DateTimeStamp::from_datetime(DateTime::<Utc>::from_timestamp(0, 0).unwrap());
|
||||
let token = create_link(&repo, Some(past), None).await;
|
||||
|
||||
let handler = AccessSharedResourceHandler::new(repo);
|
||||
let result = handler.execute(AccessSharedResourceQuery { token }).await;
|
||||
assert!(matches!(result, Err(DomainError::Forbidden(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exhausted_link_rejected() {
|
||||
let repo = Arc::new(InMemoryShareRepository::new());
|
||||
let token = create_link(&repo, None, Some(1)).await;
|
||||
|
||||
// Use it once
|
||||
let handler = AccessSharedResourceHandler::new(repo.clone());
|
||||
handler.execute(AccessSharedResourceQuery { token: token.clone() }).await.unwrap();
|
||||
|
||||
// Second use should fail
|
||||
let result = handler.execute(AccessSharedResourceQuery { token }).await;
|
||||
assert!(matches!(result, Err(DomainError::Forbidden(_))));
|
||||
}
|
||||
1
crates/application/tests/sharing/queries/mod.rs
Normal file
1
crates/application/tests/sharing/queries/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod access_shared_resource;
|
||||
Reference in New Issue
Block a user