refactor: restructure domain crate by bounded context

This commit is contained in:
2026-05-31 04:44:48 +02:00
parent 2b62d1ec81
commit de93373b43
136 changed files with 2111 additions and 2096 deletions

View 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);
}

View File

@@ -0,0 +1,2 @@
mod entities;
mod services;

View File

@@ -0,0 +1,2 @@
mod value_objects;
mod events;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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"));
}

View File

@@ -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(_))));
}

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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(_))));
}

View File

@@ -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(_))));
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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());
}

View File

@@ -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");
}

View 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(_))));
}

View File

@@ -0,0 +1,2 @@
mod entities;
mod services;

View File

@@ -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,

View File

@@ -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());
}

View File

@@ -0,0 +1 @@
mod entities;

View 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);
}

View File

@@ -0,0 +1 @@
mod entities;

View File

@@ -1,3 +0,0 @@
mod permission_service;
mod quota_checker;
mod metadata_resolver;

View 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());
}

View File

@@ -0,0 +1 @@
mod entities;

View File

@@ -0,0 +1 @@
mod entities;

View File

@@ -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);
}

View File

@@ -0,0 +1,2 @@
mod entities;
mod services;