refactor: restructure domain crate by bounded context
This commit is contained in:
117
crates/domain/tests/catalog/entities.rs
Normal file
117
crates/domain/tests/catalog/entities.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use domain::entities::{
|
||||
Asset, AssetMetadata, AssetStack, AssetType, DerivativeAsset, DerivativeProfile,
|
||||
DetectionMethod, DuplicateGroup, DuplicateStatus, GenerationStatus,
|
||||
MetadataSource, SourceReference, StackMemberRole, StackType,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{Checksum, MetadataValue, StructuredData, SystemId};
|
||||
|
||||
// --- Asset ---
|
||||
|
||||
fn make_asset() -> Asset {
|
||||
let src = SourceReference {
|
||||
volume_id: SystemId::new(),
|
||||
relative_path: "photos/img.jpg".to_string(),
|
||||
checksum: Checksum::new("a".repeat(64)).unwrap(),
|
||||
};
|
||||
Asset::new(src, AssetType::Image, "image/jpeg", 1024, SystemId::new())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_asset_is_unprocessed() {
|
||||
let a = make_asset();
|
||||
assert!(!a.is_processed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_processed() {
|
||||
let mut a = make_asset();
|
||||
a.mark_processed();
|
||||
assert!(a.is_processed);
|
||||
}
|
||||
|
||||
// --- AssetMetadata ---
|
||||
|
||||
#[test]
|
||||
fn metadata_source_ordering() {
|
||||
assert!(MetadataSource::ExifExtracted < MetadataSource::AiGenerated);
|
||||
assert!(MetadataSource::AiGenerated < MetadataSource::UserEdited);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_metadata_layer() {
|
||||
let mut data = StructuredData::new();
|
||||
data.insert("camera", MetadataValue::String("Canon".to_string()));
|
||||
let meta = AssetMetadata::new(SystemId::new(), MetadataSource::ExifExtracted, data);
|
||||
assert_eq!(meta.metadata_source, MetadataSource::ExifExtracted);
|
||||
assert_eq!(meta.data.get_string("camera"), Some("Canon"));
|
||||
}
|
||||
|
||||
// --- AssetStack ---
|
||||
|
||||
#[test]
|
||||
fn new_stack_contains_primary() {
|
||||
let primary = SystemId::new();
|
||||
let stack = AssetStack::new(StackType::LivePhoto, primary.clone(), SystemId::new());
|
||||
assert_eq!(stack.members.len(), 1);
|
||||
assert_eq!(stack.members[0].asset_id, primary);
|
||||
assert_eq!(stack.members[0].role, StackMemberRole::PrimaryDisplay);
|
||||
assert_eq!(stack.members[0].sort_order, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_motion_clip() {
|
||||
let mut stack = AssetStack::new(StackType::LivePhoto, SystemId::new(), SystemId::new());
|
||||
let clip_id = SystemId::new();
|
||||
stack.add_member(clip_id.clone(), StackMemberRole::MotionClip).unwrap();
|
||||
assert_eq!(stack.members.len(), 2);
|
||||
assert_eq!(stack.members[1].asset_id, clip_id);
|
||||
assert_eq!(stack.members[1].sort_order, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_add_duplicate_to_stack() {
|
||||
let primary = SystemId::new();
|
||||
let mut stack = AssetStack::new(StackType::LivePhoto, primary.clone(), SystemId::new());
|
||||
let result = stack.add_member(primary, StackMemberRole::HighResSource);
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
|
||||
// --- DerivativeAsset ---
|
||||
|
||||
#[test]
|
||||
fn derivative_lifecycle() {
|
||||
let mut d = DerivativeAsset::new_pending(
|
||||
SystemId::new(),
|
||||
DerivativeProfile::ThumbnailSquare,
|
||||
"/thumbs/abc.webp",
|
||||
);
|
||||
assert_eq!(d.generation_status, GenerationStatus::Pending);
|
||||
assert_eq!(d.file_size, 0);
|
||||
assert_eq!(d.dimensions, (0, 0));
|
||||
|
||||
d.mark_ready("image/webp", 4096, (256, 256));
|
||||
assert_eq!(d.generation_status, GenerationStatus::Ready);
|
||||
assert_eq!(d.mime_type, "image/webp");
|
||||
assert_eq!(d.file_size, 4096);
|
||||
assert_eq!(d.dimensions, (256, 256));
|
||||
}
|
||||
|
||||
// --- Duplicate ---
|
||||
|
||||
#[test]
|
||||
fn exact_duplicate_group() {
|
||||
let g = DuplicateGroup::new_exact(SystemId::new(), SystemId::new());
|
||||
assert_eq!(g.detection_method, DetectionMethod::ExactHash);
|
||||
assert_eq!(g.status, DuplicateStatus::Unresolved);
|
||||
assert_eq!(g.candidates.len(), 2);
|
||||
assert_eq!(g.candidates[0].similarity_score, 1.0);
|
||||
assert_eq!(g.candidates[1].similarity_score, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_group() {
|
||||
let mut g = DuplicateGroup::new_exact(SystemId::new(), SystemId::new());
|
||||
g.resolve();
|
||||
assert_eq!(g.status, DuplicateStatus::Resolved);
|
||||
}
|
||||
2
crates/domain/tests/catalog/mod.rs
Normal file
2
crates/domain/tests/catalog/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod entities;
|
||||
mod services;
|
||||
2
crates/domain/tests/common/mod.rs
Normal file
2
crates/domain/tests/common/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod value_objects;
|
||||
mod events;
|
||||
@@ -1,4 +1,8 @@
|
||||
mod entities;
|
||||
mod events;
|
||||
mod services;
|
||||
mod value_objects;
|
||||
mod common;
|
||||
mod identity;
|
||||
mod storage;
|
||||
mod catalog;
|
||||
mod organization;
|
||||
mod sharing;
|
||||
mod sidecar;
|
||||
mod processing;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use domain::entities::{Asset, AssetType, SourceReference};
|
||||
use domain::value_objects::{Checksum, SystemId};
|
||||
|
||||
fn make_asset() -> Asset {
|
||||
let src = SourceReference {
|
||||
volume_id: SystemId::new(),
|
||||
relative_path: "photos/img.jpg".to_string(),
|
||||
checksum: Checksum::new("a".repeat(64)).unwrap(),
|
||||
};
|
||||
Asset::new(src, AssetType::Image, "image/jpeg", 1024, SystemId::new())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_asset_is_unprocessed() {
|
||||
let a = make_asset();
|
||||
assert!(!a.is_processed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_processed() {
|
||||
let mut a = make_asset();
|
||||
a.mark_processed();
|
||||
assert!(a.is_processed);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use domain::entities::{AssetMetadata, MetadataSource};
|
||||
use domain::value_objects::{MetadataValue, StructuredData, SystemId};
|
||||
|
||||
#[test]
|
||||
fn metadata_source_ordering() {
|
||||
assert!(MetadataSource::ExifExtracted < MetadataSource::AiGenerated);
|
||||
assert!(MetadataSource::AiGenerated < MetadataSource::UserEdited);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_metadata_layer() {
|
||||
let mut data = StructuredData::new();
|
||||
data.insert("camera", MetadataValue::String("Canon".to_string()));
|
||||
let meta = AssetMetadata::new(SystemId::new(), MetadataSource::ExifExtracted, data);
|
||||
assert_eq!(meta.metadata_source, MetadataSource::ExifExtracted);
|
||||
assert_eq!(meta.data.get_string("camera"), Some("Canon"));
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use domain::entities::{AssetStack, StackMemberRole, StackType};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn new_stack_contains_primary() {
|
||||
let primary = SystemId::new();
|
||||
let stack = AssetStack::new(StackType::LivePhoto, primary.clone(), SystemId::new());
|
||||
assert_eq!(stack.members.len(), 1);
|
||||
assert_eq!(stack.members[0].asset_id, primary);
|
||||
assert_eq!(stack.members[0].role, StackMemberRole::PrimaryDisplay);
|
||||
assert_eq!(stack.members[0].sort_order, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_motion_clip() {
|
||||
let mut stack = AssetStack::new(StackType::LivePhoto, SystemId::new(), SystemId::new());
|
||||
let clip_id = SystemId::new();
|
||||
stack.add_member(clip_id.clone(), StackMemberRole::MotionClip).unwrap();
|
||||
assert_eq!(stack.members.len(), 2);
|
||||
assert_eq!(stack.members[1].asset_id, clip_id);
|
||||
assert_eq!(stack.members[1].sort_order, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_add_duplicate() {
|
||||
let primary = SystemId::new();
|
||||
let mut stack = AssetStack::new(StackType::LivePhoto, primary.clone(), SystemId::new());
|
||||
let result = stack.add_member(primary, StackMemberRole::HighResSource);
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use domain::entities::{DerivativeAsset, DerivativeProfile, GenerationStatus};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn lifecycle() {
|
||||
let mut d = DerivativeAsset::new_pending(
|
||||
SystemId::new(),
|
||||
DerivativeProfile::ThumbnailSquare,
|
||||
"/thumbs/abc.webp",
|
||||
);
|
||||
assert_eq!(d.generation_status, GenerationStatus::Pending);
|
||||
assert_eq!(d.file_size, 0);
|
||||
assert_eq!(d.dimensions, (0, 0));
|
||||
|
||||
d.mark_ready("image/webp", 4096, (256, 256));
|
||||
assert_eq!(d.generation_status, GenerationStatus::Ready);
|
||||
assert_eq!(d.mime_type, "image/webp");
|
||||
assert_eq!(d.file_size, 4096);
|
||||
assert_eq!(d.dimensions, (256, 256));
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use domain::entities::{DetectionMethod, DuplicateGroup, DuplicateStatus};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn exact_duplicate_group() {
|
||||
let g = DuplicateGroup::new_exact(SystemId::new(), SystemId::new());
|
||||
assert_eq!(g.detection_method, DetectionMethod::ExactHash);
|
||||
assert_eq!(g.status, DuplicateStatus::Unresolved);
|
||||
assert_eq!(g.candidates.len(), 2);
|
||||
assert_eq!(g.candidates[0].similarity_score, 1.0);
|
||||
assert_eq!(g.candidates[1].similarity_score, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_group() {
|
||||
let mut g = DuplicateGroup::new_exact(SystemId::new(), SystemId::new());
|
||||
g.resolve();
|
||||
assert_eq!(g.status, DuplicateStatus::Resolved);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use domain::entities::Group;
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn owner_auto_member() {
|
||||
let owner = SystemId::new();
|
||||
let g = Group::new("team", owner);
|
||||
assert!(g.is_member(&owner));
|
||||
assert_eq!(g.members.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_remove() {
|
||||
let owner = SystemId::new();
|
||||
let member = SystemId::new();
|
||||
let mut g = Group::new("team", owner);
|
||||
g.add_member(member).unwrap();
|
||||
assert!(g.is_member(&member));
|
||||
assert_eq!(g.members.len(), 2);
|
||||
g.remove_member(member).unwrap();
|
||||
assert!(!g.is_member(&member));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_remove_owner() {
|
||||
let owner = SystemId::new();
|
||||
let mut g = Group::new("team", owner);
|
||||
let result = g.remove_member(owner);
|
||||
assert!(matches!(result, Err(DomainError::Validation(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_add_duplicate() {
|
||||
let owner = SystemId::new();
|
||||
let member = SystemId::new();
|
||||
let mut g = Group::new("team", owner);
|
||||
g.add_member(member).unwrap();
|
||||
let result = g.add_member(member);
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use domain::entities::{Job, JobStatus, JobType};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::StructuredData;
|
||||
|
||||
#[test]
|
||||
fn job_lifecycle_success() {
|
||||
let mut job = Job::new(JobType::ExtractMetadata, 5, StructuredData::new());
|
||||
assert_eq!(job.status, JobStatus::Queued);
|
||||
|
||||
job.start().unwrap();
|
||||
assert_eq!(job.status, JobStatus::Processing);
|
||||
assert!(job.started_at.is_some());
|
||||
|
||||
job.complete(StructuredData::new());
|
||||
assert_eq!(job.status, JobStatus::Completed);
|
||||
assert!(job.result_data.is_some());
|
||||
assert!(job.completed_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_on_failure() {
|
||||
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||
job.start().unwrap();
|
||||
|
||||
job.fail("timeout");
|
||||
assert_eq!(job.status, JobStatus::Queued);
|
||||
assert_eq!(job.retry_count, 1);
|
||||
assert!(job.can_retry());
|
||||
assert!(job.started_at.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_after_max_retries() {
|
||||
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||
job.max_retries = 2;
|
||||
|
||||
job.start().unwrap();
|
||||
job.fail("err1");
|
||||
assert_eq!(job.status, JobStatus::Queued);
|
||||
|
||||
job.start().unwrap();
|
||||
job.fail("err2");
|
||||
assert_eq!(job.status, JobStatus::Failed);
|
||||
assert!(!job.can_retry());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_start_from_processing() {
|
||||
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||
job.start().unwrap();
|
||||
let result = job.start();
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use domain::entities::{BatchStatus, JobBatch};
|
||||
|
||||
#[test]
|
||||
fn completes_when_all_done() {
|
||||
let mut batch = JobBatch::new("scan", 3);
|
||||
batch.record_completion();
|
||||
batch.record_completion();
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.status, BatchStatus::Completed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_with_errors() {
|
||||
let mut batch = JobBatch::new("scan", 3);
|
||||
batch.record_completion();
|
||||
batch.record_failure();
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.status, BatchStatus::CompletedWithErrors);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_tracking() {
|
||||
let mut batch = JobBatch::new("scan", 4);
|
||||
assert_eq!(batch.progress_percent(), 0.0);
|
||||
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.progress_percent(), 25.0);
|
||||
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.progress_percent(), 50.0);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use domain::entities::{LibraryPath, OwnershipPolicy};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn user_owned_path() {
|
||||
let vol = SystemId::new();
|
||||
let owner = SystemId::new();
|
||||
let lp = LibraryPath::new_user_owned(vol, "/photos", owner, true);
|
||||
assert_eq!(lp.ownership_policy, OwnershipPolicy::UserOwned);
|
||||
assert_eq!(lp.designated_owner_id, Some(owner));
|
||||
assert!(lp.is_ingest_destination);
|
||||
assert_eq!(lp.volume_id, vol);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
mod group;
|
||||
mod permission;
|
||||
mod role;
|
||||
mod user;
|
||||
mod storage_volume;
|
||||
mod library_path;
|
||||
mod ingest_session;
|
||||
mod quota;
|
||||
mod asset;
|
||||
mod asset_metadata;
|
||||
mod asset_stack;
|
||||
mod derivative_asset;
|
||||
mod duplicate;
|
||||
mod album;
|
||||
mod tag;
|
||||
mod share_scope;
|
||||
mod share_link;
|
||||
mod sidecar_record;
|
||||
mod job;
|
||||
mod job_batch;
|
||||
mod processing_pipeline;
|
||||
@@ -1,19 +0,0 @@
|
||||
use domain::entities::permission::{
|
||||
admin_permissions, contributor_permissions, viewer_permissions,
|
||||
Permission, PermissionAction, ResourceType,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn admin_is_superset_of_contributor() {
|
||||
let admin = admin_permissions();
|
||||
let contrib = contributor_permissions();
|
||||
assert!(contrib.is_subset(&admin));
|
||||
assert!(admin.len() > contrib.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewer_cannot_write() {
|
||||
let viewer = viewer_permissions();
|
||||
let write = Permission::new(PermissionAction::WriteMetadata, ResourceType::Global);
|
||||
assert!(!viewer.contains(&write));
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use domain::entities::ProcessingPipeline;
|
||||
use domain::value_objects::{StructuredData, SystemId};
|
||||
|
||||
#[test]
|
||||
fn steps_ordered() {
|
||||
let mut pipeline = ProcessingPipeline::new("asset.created");
|
||||
assert!(pipeline.steps.is_empty());
|
||||
|
||||
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||
|
||||
assert_eq!(pipeline.steps.len(), 3);
|
||||
assert_eq!(pipeline.steps[0].step_order, 0);
|
||||
assert_eq!(pipeline.steps[1].step_order, 1);
|
||||
assert_eq!(pipeline.steps[2].step_order, 2);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use domain::entities::{QuotaDefinition, TimePeriod, UsageType};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn quota_with_rules() {
|
||||
let mut q = QuotaDefinition::new(SystemId::new());
|
||||
assert!(q.is_enforced);
|
||||
assert!(q.rules.is_empty());
|
||||
|
||||
q.add_rule(UsageType::StorageBytes, 1_000_000, TimePeriod::Monthly);
|
||||
q.add_unlimited_rule(UsageType::ApiCalls);
|
||||
|
||||
assert_eq!(q.rules.len(), 2);
|
||||
assert!(!q.rules[0].is_unlimited);
|
||||
assert_eq!(q.rules[0].limit_value, 1_000_000);
|
||||
assert!(q.rules[1].is_unlimited);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use domain::entities::{Role, PermissionAction, ResourceType};
|
||||
use domain::entities::permission::viewer_permissions;
|
||||
|
||||
#[test]
|
||||
fn role_checks_permission() {
|
||||
let role = Role::new("viewer", viewer_permissions(), true);
|
||||
assert!(role.has_permission(PermissionAction::ReadAsset, ResourceType::Global));
|
||||
assert!(!role.has_permission(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use domain::entities::{LinkAccessLevel, ShareLink};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn new_link_is_valid() {
|
||||
let link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
assert!(link.is_valid());
|
||||
assert_eq!(link.use_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deactivated_invalid() {
|
||||
let mut link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
link.deactivate();
|
||||
assert!(!link.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_uses_exhausted() {
|
||||
let mut link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
link.max_uses = Some(2);
|
||||
link.record_use();
|
||||
link.record_use();
|
||||
assert!(!link.is_valid());
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use domain::entities::{ScopeType, ShareScope, ShareableType};
|
||||
use domain::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[test]
|
||||
fn not_expired_when_no_expiry() {
|
||||
let scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
assert!(!scope.is_expired());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_when_past() {
|
||||
let mut scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
scope.expires_at = Some(DateTimeStamp::from_datetime(Utc::now() - Duration::hours(1)));
|
||||
assert!(scope.is_expired());
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use domain::entities::StorageVolume;
|
||||
|
||||
#[test]
|
||||
fn creates_read_only_volume() {
|
||||
let vol = StorageVolume::new("archive", "s3://bucket/", false);
|
||||
assert_eq!(vol.volume_name, "archive");
|
||||
assert_eq!(vol.uri_prefix, "s3://bucket/");
|
||||
assert!(!vol.is_writable);
|
||||
assert_eq!(vol.available_bytes, 0);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use domain::entities::{AssetTag, Tag, TagSource};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn manual_tag_has_full_confidence() {
|
||||
let tag = Tag::new_manual("sunset");
|
||||
assert_eq!(tag.name, "sunset");
|
||||
assert_eq!(tag.tag_source, TagSource::UserManual);
|
||||
|
||||
let asset_tag = AssetTag::new_manual(SystemId::new(), tag.tag_id.clone(), SystemId::new());
|
||||
assert_eq!(asset_tag.confidence, 1.0);
|
||||
assert!(asset_tag.tagged_by_user_id.is_some());
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use domain::entities::User;
|
||||
use domain::value_objects::{Email, PasswordHash};
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_unique_id() {
|
||||
let a = User::new("alice", Email::new("a@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||
let b = User::new("bob", Email::new("b@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.username, "alice");
|
||||
assert_eq!(b.username, "bob");
|
||||
}
|
||||
84
crates/domain/tests/identity/entities.rs
Normal file
84
crates/domain/tests/identity/entities.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use domain::entities::permission::{
|
||||
admin_permissions, contributor_permissions, viewer_permissions,
|
||||
Permission, PermissionAction, ResourceType,
|
||||
};
|
||||
use domain::entities::{Group, Role, User};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{Email, PasswordHash, SystemId};
|
||||
|
||||
// --- Permission ---
|
||||
|
||||
#[test]
|
||||
fn admin_is_superset_of_contributor() {
|
||||
let admin = admin_permissions();
|
||||
let contrib = contributor_permissions();
|
||||
assert!(contrib.is_subset(&admin));
|
||||
assert!(admin.len() > contrib.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewer_cannot_write() {
|
||||
let viewer = viewer_permissions();
|
||||
let write = Permission::new(PermissionAction::WriteMetadata, ResourceType::Global);
|
||||
assert!(!viewer.contains(&write));
|
||||
}
|
||||
|
||||
// --- Role ---
|
||||
|
||||
#[test]
|
||||
fn role_checks_permission() {
|
||||
let role = Role::new("viewer", viewer_permissions(), true);
|
||||
assert!(role.has_permission(PermissionAction::ReadAsset, ResourceType::Global));
|
||||
assert!(!role.has_permission(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||
}
|
||||
|
||||
// --- User ---
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_unique_id() {
|
||||
let a = User::new("alice", Email::new("a@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||
let b = User::new("bob", Email::new("b@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.username, "alice");
|
||||
assert_eq!(b.username, "bob");
|
||||
}
|
||||
|
||||
// --- Group ---
|
||||
|
||||
#[test]
|
||||
fn owner_auto_member() {
|
||||
let owner = SystemId::new();
|
||||
let g = Group::new("team", owner);
|
||||
assert!(g.is_member(&owner));
|
||||
assert_eq!(g.members.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_remove() {
|
||||
let owner = SystemId::new();
|
||||
let member = SystemId::new();
|
||||
let mut g = Group::new("team", owner);
|
||||
g.add_member(member).unwrap();
|
||||
assert!(g.is_member(&member));
|
||||
assert_eq!(g.members.len(), 2);
|
||||
g.remove_member(member).unwrap();
|
||||
assert!(!g.is_member(&member));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_remove_owner() {
|
||||
let owner = SystemId::new();
|
||||
let mut g = Group::new("team", owner);
|
||||
let result = g.remove_member(owner);
|
||||
assert!(matches!(result, Err(DomainError::Validation(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_add_duplicate() {
|
||||
let owner = SystemId::new();
|
||||
let member = SystemId::new();
|
||||
let mut g = Group::new("team", owner);
|
||||
g.add_member(member).unwrap();
|
||||
let result = g.add_member(member);
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
2
crates/domain/tests/identity/mod.rs
Normal file
2
crates/domain/tests/identity/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod entities;
|
||||
mod services;
|
||||
@@ -33,7 +33,6 @@ fn roles_additive() {
|
||||
#[test]
|
||||
fn global_covers_specific() {
|
||||
let role = Role::new("admin", admin_permissions(), true);
|
||||
// Global ReadAsset should cover Asset-scoped check
|
||||
assert!(PermissionChecker::has_permission(
|
||||
&[role],
|
||||
PermissionAction::ReadAsset,
|
||||
@@ -1,7 +1,9 @@
|
||||
use domain::entities::Album;
|
||||
use domain::entities::{Album, AssetTag, Tag, TagSource};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
// --- Album ---
|
||||
|
||||
#[test]
|
||||
fn add_and_remove_asset() {
|
||||
let mut album = Album::new("Vacation", SystemId::new());
|
||||
@@ -25,3 +27,16 @@ fn cannot_add_duplicate() {
|
||||
let result = album.add_asset(asset, user);
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
|
||||
// --- Tag ---
|
||||
|
||||
#[test]
|
||||
fn manual_tag_has_full_confidence() {
|
||||
let tag = Tag::new_manual("sunset");
|
||||
assert_eq!(tag.name, "sunset");
|
||||
assert_eq!(tag.tag_source, TagSource::UserManual);
|
||||
|
||||
let asset_tag = AssetTag::new_manual(SystemId::new(), tag.tag_id.clone(), SystemId::new());
|
||||
assert_eq!(asset_tag.confidence, 1.0);
|
||||
assert!(asset_tag.tagged_by_user_id.is_some());
|
||||
}
|
||||
1
crates/domain/tests/organization/mod.rs
Normal file
1
crates/domain/tests/organization/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod entities;
|
||||
104
crates/domain/tests/processing/entities.rs
Normal file
104
crates/domain/tests/processing/entities.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use domain::entities::{BatchStatus, Job, JobBatch, JobStatus, JobType, ProcessingPipeline};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{StructuredData, SystemId};
|
||||
|
||||
// --- Job ---
|
||||
|
||||
#[test]
|
||||
fn job_lifecycle_success() {
|
||||
let mut job = Job::new(JobType::ExtractMetadata, 5, StructuredData::new());
|
||||
assert_eq!(job.status, JobStatus::Queued);
|
||||
|
||||
job.start().unwrap();
|
||||
assert_eq!(job.status, JobStatus::Processing);
|
||||
assert!(job.started_at.is_some());
|
||||
|
||||
job.complete(StructuredData::new());
|
||||
assert_eq!(job.status, JobStatus::Completed);
|
||||
assert!(job.result_data.is_some());
|
||||
assert!(job.completed_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_on_failure() {
|
||||
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||
job.start().unwrap();
|
||||
|
||||
job.fail("timeout");
|
||||
assert_eq!(job.status, JobStatus::Queued);
|
||||
assert_eq!(job.retry_count, 1);
|
||||
assert!(job.can_retry());
|
||||
assert!(job.started_at.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_after_max_retries() {
|
||||
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||
job.max_retries = 2;
|
||||
|
||||
job.start().unwrap();
|
||||
job.fail("err1");
|
||||
assert_eq!(job.status, JobStatus::Queued);
|
||||
|
||||
job.start().unwrap();
|
||||
job.fail("err2");
|
||||
assert_eq!(job.status, JobStatus::Failed);
|
||||
assert!(!job.can_retry());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_start_from_processing() {
|
||||
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||
job.start().unwrap();
|
||||
let result = job.start();
|
||||
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||
}
|
||||
|
||||
// --- JobBatch ---
|
||||
|
||||
#[test]
|
||||
fn completes_when_all_done() {
|
||||
let mut batch = JobBatch::new("scan", 3);
|
||||
batch.record_completion();
|
||||
batch.record_completion();
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.status, BatchStatus::Completed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_with_errors() {
|
||||
let mut batch = JobBatch::new("scan", 3);
|
||||
batch.record_completion();
|
||||
batch.record_failure();
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.status, BatchStatus::CompletedWithErrors);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_tracking() {
|
||||
let mut batch = JobBatch::new("scan", 4);
|
||||
assert_eq!(batch.progress_percent(), 0.0);
|
||||
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.progress_percent(), 25.0);
|
||||
|
||||
batch.record_completion();
|
||||
assert_eq!(batch.progress_percent(), 50.0);
|
||||
}
|
||||
|
||||
// --- ProcessingPipeline ---
|
||||
|
||||
#[test]
|
||||
fn steps_ordered() {
|
||||
let mut pipeline = ProcessingPipeline::new("asset.created");
|
||||
assert!(pipeline.steps.is_empty());
|
||||
|
||||
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||
|
||||
assert_eq!(pipeline.steps.len(), 3);
|
||||
assert_eq!(pipeline.steps[0].step_order, 0);
|
||||
assert_eq!(pipeline.steps[1].step_order, 1);
|
||||
assert_eq!(pipeline.steps[2].step_order, 2);
|
||||
}
|
||||
1
crates/domain/tests/processing/mod.rs
Normal file
1
crates/domain/tests/processing/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod entities;
|
||||
@@ -1,3 +0,0 @@
|
||||
mod permission_service;
|
||||
mod quota_checker;
|
||||
mod metadata_resolver;
|
||||
43
crates/domain/tests/sharing/entities.rs
Normal file
43
crates/domain/tests/sharing/entities.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use domain::entities::{LinkAccessLevel, ScopeType, ShareLink, ShareScope, ShareableType};
|
||||
use domain::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
// --- ShareScope ---
|
||||
|
||||
#[test]
|
||||
fn not_expired_when_no_expiry() {
|
||||
let scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
assert!(!scope.is_expired());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_when_past() {
|
||||
let mut scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
scope.expires_at = Some(DateTimeStamp::from_datetime(Utc::now() - Duration::hours(1)));
|
||||
assert!(scope.is_expired());
|
||||
}
|
||||
|
||||
// --- ShareLink ---
|
||||
|
||||
#[test]
|
||||
fn new_link_is_valid() {
|
||||
let link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
assert!(link.is_valid());
|
||||
assert_eq!(link.use_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deactivated_invalid() {
|
||||
let mut link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
link.deactivate();
|
||||
assert!(!link.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_uses_exhausted() {
|
||||
let mut link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
link.max_uses = Some(2);
|
||||
link.record_use();
|
||||
link.record_use();
|
||||
assert!(!link.is_valid());
|
||||
}
|
||||
1
crates/domain/tests/sharing/mod.rs
Normal file
1
crates/domain/tests/sharing/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod entities;
|
||||
1
crates/domain/tests/sidecar/mod.rs
Normal file
1
crates/domain/tests/sidecar/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod entities;
|
||||
@@ -1,7 +1,36 @@
|
||||
use domain::entities::{IngestSession, IngestStatus};
|
||||
use domain::entities::{
|
||||
IngestSession, IngestStatus, LibraryPath, OwnershipPolicy,
|
||||
QuotaDefinition, StorageVolume, TimePeriod, UsageType,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{Checksum, SystemId};
|
||||
|
||||
// --- StorageVolume ---
|
||||
|
||||
#[test]
|
||||
fn creates_read_only_volume() {
|
||||
let vol = StorageVolume::new("archive", "s3://bucket/", false);
|
||||
assert_eq!(vol.volume_name, "archive");
|
||||
assert_eq!(vol.uri_prefix, "s3://bucket/");
|
||||
assert!(!vol.is_writable);
|
||||
assert_eq!(vol.available_bytes, 0);
|
||||
}
|
||||
|
||||
// --- LibraryPath ---
|
||||
|
||||
#[test]
|
||||
fn user_owned_path() {
|
||||
let vol = SystemId::new();
|
||||
let owner = SystemId::new();
|
||||
let lp = LibraryPath::new_user_owned(vol, "/photos", owner, true);
|
||||
assert_eq!(lp.ownership_policy, OwnershipPolicy::UserOwned);
|
||||
assert_eq!(lp.designated_owner_id, Some(owner));
|
||||
assert!(lp.is_ingest_destination);
|
||||
assert_eq!(lp.volume_id, vol);
|
||||
}
|
||||
|
||||
// --- IngestSession ---
|
||||
|
||||
fn make_session() -> IngestSession {
|
||||
let checksum = Checksum::new("a".repeat(64)).unwrap();
|
||||
IngestSession::new(
|
||||
@@ -36,7 +65,6 @@ fn invalid_transition_rejected() {
|
||||
fn can_fail_from_any_non_terminal() {
|
||||
for target in [IngestStatus::Uploading, IngestStatus::AwaitingProcessing, IngestStatus::Processing] {
|
||||
let mut s = make_session();
|
||||
// advance to target state
|
||||
if target == IngestStatus::AwaitingProcessing || target == IngestStatus::Processing {
|
||||
s.advance_to(IngestStatus::AwaitingProcessing).unwrap();
|
||||
}
|
||||
@@ -47,3 +75,20 @@ fn can_fail_from_any_non_terminal() {
|
||||
assert_eq!(s.status, IngestStatus::Failed);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Quota ---
|
||||
|
||||
#[test]
|
||||
fn quota_with_rules() {
|
||||
let mut q = QuotaDefinition::new(SystemId::new());
|
||||
assert!(q.is_enforced);
|
||||
assert!(q.rules.is_empty());
|
||||
|
||||
q.add_rule(UsageType::StorageBytes, 1_000_000, TimePeriod::Monthly);
|
||||
q.add_unlimited_rule(UsageType::ApiCalls);
|
||||
|
||||
assert_eq!(q.rules.len(), 2);
|
||||
assert!(!q.rules[0].is_unlimited);
|
||||
assert_eq!(q.rules[0].limit_value, 1_000_000);
|
||||
assert!(q.rules[1].is_unlimited);
|
||||
}
|
||||
2
crates/domain/tests/storage/mod.rs
Normal file
2
crates/domain/tests/storage/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod entities;
|
||||
mod services;
|
||||
Reference in New Issue
Block a user