add 400+ unit tests for domain and application layers
Some checks failed
CI / Check / Test (push) Has been cancelled

Extract ReviewLogger trait to decouple import/integrations
from diary::log_review (cross-module coupling smell).

Add in-memory fakes for all repository ports, enabling
isolated testing of every use case module without a database.

Coverage: domain+application 22% → 80%, 427 tests.
This commit is contained in:
2026-06-09 02:07:26 +02:00
parent 30a6200b5b
commit d867a14b28
122 changed files with 7033 additions and 151 deletions

View File

@@ -12,3 +12,7 @@ pub async fn execute(ctx: &AppContext, id: WrapUpId) -> Result<(), DomainError>
ctx.repos.wrapup_repo.delete(&id).await
}
#[cfg(test)]
#[path = "tests/delete.rs"]
mod tests;

View File

@@ -58,3 +58,7 @@ pub async fn execute(ctx: &AppContext, cmd: RequestWrapUpCommand) -> Result<Wrap
Ok(id)
}
#[cfg(test)]
#[path = "tests/generate.rs"]
mod tests;

View File

@@ -7,3 +7,7 @@ use crate::context::AppContext;
pub async fn execute(ctx: &AppContext, id: WrapUpId) -> Result<Option<WrapUpRecord>, DomainError> {
ctx.repos.wrapup_repo.get_by_id(&id).await
}
#[cfg(test)]
#[path = "tests/get_wrapup.rs"]
mod tests;

View File

@@ -59,3 +59,7 @@ pub async fn execute(
}
}
}
#[cfg(test)]
#[path = "tests/handle_requested.rs"]
mod tests;

View File

@@ -18,3 +18,7 @@ pub async fn execute(
None => ctx.repos.wrapup_repo.list_global().await,
}
}
#[cfg(test)]
#[path = "tests/list_wrapups.rs"]
mod tests;

View File

@@ -0,0 +1,45 @@
use std::sync::Arc;
use chrono::NaiveDate;
use domain::models::wrapup::{WrapUpRecord, WrapUpStatus};
use domain::testing::InMemoryWrapUpRepository;
use domain::value_objects::WrapUpId;
use crate::test_helpers::TestContextBuilder;
use crate::wrapup::delete;
#[tokio::test]
async fn deletes_existing_wrapup() {
let repo = InMemoryWrapUpRepository::new();
let id = WrapUpId::generate();
repo.store.lock().unwrap().push(WrapUpRecord {
id: id.clone(),
user_id: None,
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
status: WrapUpStatus::Ready,
report: None,
error_message: None,
created_at: chrono::Utc::now().naive_utc(),
completed_at: None,
});
let ctx = TestContextBuilder::new().build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
..ctx
};
delete::execute(&ctx, id).await.unwrap();
assert_eq!(repo.store.lock().unwrap().len(), 0);
}
#[tokio::test]
async fn fails_when_not_found() {
let ctx = TestContextBuilder::new().build();
let result = delete::execute(&ctx, WrapUpId::generate()).await;
assert!(result.is_err());
}

View File

@@ -0,0 +1,137 @@
use std::sync::Arc;
use chrono::NaiveDate;
use domain::events::DomainEvent;
use domain::models::wrapup::{WrapUpRecord, WrapUpStatus};
use domain::testing::{InMemoryWrapUpRepository, NoopEventPublisher};
use domain::value_objects::WrapUpId;
use uuid::Uuid;
use crate::test_helpers::TestContextBuilder;
use crate::wrapup::{commands::RequestWrapUpCommand, generate};
fn past_cmd() -> RequestWrapUpCommand {
RequestWrapUpCommand {
user_id: Some(Uuid::nil()),
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
}
}
#[tokio::test]
async fn creates_pending_record_and_emits_event() {
let repo = InMemoryWrapUpRepository::new();
let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new()
.wrapup_stats(domain::testing::InMemoryWrapUpStatsQuery::new())
.build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
services: crate::context::Services {
event_publisher: Arc::clone(&events) as _,
..ctx.services
},
config: ctx.config,
};
let id = generate::execute(&ctx, past_cmd()).await.unwrap();
let stored = repo.store.lock().unwrap();
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].id, id);
assert_eq!(stored[0].status, WrapUpStatus::Pending);
let published = events.published();
assert!(
published
.iter()
.any(|e| matches!(e, DomainEvent::WrapUpRequested { .. }))
);
}
#[tokio::test]
async fn reuses_existing_ready_wrapup() {
let repo = InMemoryWrapUpRepository::new();
let existing_id = WrapUpId::generate();
repo.store.lock().unwrap().push(WrapUpRecord {
id: existing_id.clone(),
user_id: Some(Uuid::nil()),
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
status: WrapUpStatus::Ready,
report: None,
error_message: None,
created_at: chrono::Utc::now().naive_utc(),
completed_at: None,
});
let ctx = TestContextBuilder::new().build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
..ctx
};
let id = generate::execute(&ctx, past_cmd()).await.unwrap();
assert_eq!(id, existing_id);
assert_eq!(repo.store.lock().unwrap().len(), 1);
}
#[tokio::test]
async fn replaces_failed_wrapup() {
let repo = InMemoryWrapUpRepository::new();
repo.store.lock().unwrap().push(WrapUpRecord {
id: WrapUpId::generate(),
user_id: Some(Uuid::nil()),
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
status: WrapUpStatus::Failed,
report: None,
error_message: Some("boom".into()),
created_at: chrono::Utc::now().naive_utc(),
completed_at: None,
});
let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new().build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
services: crate::context::Services {
event_publisher: Arc::clone(&events) as _,
..ctx.services
},
config: ctx.config,
};
let id = generate::execute(&ctx, past_cmd()).await.unwrap();
let stored = repo.store.lock().unwrap();
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].id, id);
assert_eq!(stored[0].status, WrapUpStatus::Pending);
}
#[tokio::test]
async fn rejects_future_end_date() {
let ctx = TestContextBuilder::new().build();
let err = generate::execute(
&ctx,
RequestWrapUpCommand {
user_id: None,
start_date: NaiveDate::from_ymd_opt(2030, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2031, 1, 1).unwrap(),
},
)
.await
.unwrap_err();
assert!(err.to_string().contains("future"));
}

View File

@@ -0,0 +1,48 @@
use std::sync::Arc;
use chrono::NaiveDate;
use domain::models::wrapup::{WrapUpRecord, WrapUpStatus};
use domain::testing::InMemoryWrapUpRepository;
use domain::value_objects::WrapUpId;
use crate::test_helpers::TestContextBuilder;
use crate::wrapup::get_wrapup;
#[tokio::test]
async fn returns_record_when_exists() {
let repo = InMemoryWrapUpRepository::new();
let id = WrapUpId::generate();
repo.store.lock().unwrap().push(WrapUpRecord {
id: id.clone(),
user_id: None,
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
status: WrapUpStatus::Pending,
report: None,
error_message: None,
created_at: chrono::Utc::now().naive_utc(),
completed_at: None,
});
let ctx = TestContextBuilder::new().build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
..ctx
};
let result = get_wrapup::execute(&ctx, id).await.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().status, WrapUpStatus::Pending);
}
#[tokio::test]
async fn returns_none_when_missing() {
let ctx = TestContextBuilder::new().build();
let result = get_wrapup::execute(&ctx, WrapUpId::generate())
.await
.unwrap();
assert!(result.is_none());
}

View File

@@ -0,0 +1,134 @@
use std::sync::Arc;
use chrono::{NaiveDate, Utc};
use domain::models::wrapup::{WrapUpRecord, WrapUpStatus};
use domain::ports::WrapUpRepository;
use domain::testing::InMemoryWrapUpRepository;
use domain::value_objects::WrapUpId;
use crate::test_helpers::TestContextBuilder;
use crate::wrapup::handle_requested;
#[tokio::test]
async fn skips_if_already_ready() {
let repo = InMemoryWrapUpRepository::new();
let wrapup_id = WrapUpId::generate();
let record = WrapUpRecord {
id: wrapup_id.clone(),
user_id: None,
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
status: WrapUpStatus::Ready,
report: None,
error_message: None,
created_at: Utc::now().naive_utc(),
completed_at: None,
};
repo.create(&record).await.unwrap();
let ctx = TestContextBuilder::new()
.with_wrapup_repo(Arc::clone(&repo) as _)
.build();
let result = handle_requested::execute(
&ctx,
wrapup_id,
None,
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn generates_wrapup_and_marks_complete() {
let repo = InMemoryWrapUpRepository::new();
let stats = domain::testing::InMemoryWrapUpStatsQuery::new();
let events = domain::testing::NoopEventPublisher::new();
let wrapup_id = WrapUpId::generate();
let uid = uuid::Uuid::new_v4();
let record = WrapUpRecord {
id: wrapup_id.clone(),
user_id: Some(uid),
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
status: WrapUpStatus::Pending,
report: None,
error_message: None,
created_at: Utc::now().naive_utc(),
completed_at: None,
};
repo.create(&record).await.unwrap();
let ctx = TestContextBuilder::new()
.with_wrapup_repo(Arc::clone(&repo) as _)
.wrapup_stats(Arc::clone(&stats) as _)
.with_event_publisher(Arc::clone(&events) as _)
.build();
let result = handle_requested::execute(
&ctx,
wrapup_id.clone(),
Some(uid),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
)
.await;
assert!(result.is_ok());
// Verify it was marked as Ready
let final_rec = repo.get_by_id(&wrapup_id).await.unwrap().unwrap();
assert_eq!(final_rec.status, WrapUpStatus::Ready);
assert!(final_rec.report.is_some());
// Verify event was published
let published = events.published();
assert!(
published
.iter()
.any(|e| matches!(e, domain::events::DomainEvent::WrapUpCompleted { .. }))
);
}
#[tokio::test]
async fn skips_if_already_generating() {
let repo = InMemoryWrapUpRepository::new();
let wrapup_id = WrapUpId::generate();
let record = WrapUpRecord {
id: wrapup_id.clone(),
user_id: None,
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
status: WrapUpStatus::Generating,
report: None,
error_message: None,
created_at: Utc::now().naive_utc(),
completed_at: None,
};
repo.create(&record).await.unwrap();
let ctx = TestContextBuilder::new()
.with_wrapup_repo(Arc::clone(&repo) as _)
.build();
let result = handle_requested::execute(
&ctx,
wrapup_id.clone(),
None,
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
)
.await;
assert!(result.is_ok());
// Status should still be Generating (not changed to Ready)
let final_rec = repo.get_by_id(&wrapup_id).await.unwrap().unwrap();
assert_eq!(final_rec.status, WrapUpStatus::Generating);
}

View File

@@ -0,0 +1,76 @@
use std::sync::Arc;
use chrono::NaiveDate;
use domain::models::wrapup::{WrapUpRecord, WrapUpStatus};
use domain::testing::InMemoryWrapUpRepository;
use domain::value_objects::WrapUpId;
use uuid::Uuid;
use crate::test_helpers::TestContextBuilder;
use crate::wrapup::list_wrapups::{self, ListWrapUpsQuery};
fn make_record(user_id: Option<Uuid>) -> WrapUpRecord {
WrapUpRecord {
id: WrapUpId::generate(),
user_id,
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
status: WrapUpStatus::Ready,
report: None,
error_message: None,
created_at: chrono::Utc::now().naive_utc(),
completed_at: None,
}
}
#[tokio::test]
async fn filters_by_user() {
let repo = InMemoryWrapUpRepository::new();
let uid = Uuid::new_v4();
{
let mut store = repo.store.lock().unwrap();
store.push(make_record(Some(uid)));
store.push(make_record(Some(Uuid::new_v4())));
store.push(make_record(None));
}
let ctx = TestContextBuilder::new().build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
..ctx
};
let result = list_wrapups::execute(&ctx, ListWrapUpsQuery { user_id: Some(uid) })
.await
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].user_id, Some(uid));
}
#[tokio::test]
async fn returns_global_when_no_user() {
let repo = InMemoryWrapUpRepository::new();
{
let mut store = repo.store.lock().unwrap();
store.push(make_record(None));
store.push(make_record(None));
store.push(make_record(Some(Uuid::new_v4())));
}
let ctx = TestContextBuilder::new().build();
let ctx = crate::context::AppContext {
repos: crate::context::Repositories {
wrapup_repo: Arc::clone(&repo) as _,
..ctx.repos
},
..ctx
};
let result = list_wrapups::execute(&ctx, ListWrapUpsQuery { user_id: None })
.await
.unwrap();
assert_eq!(result.len(), 2);
}