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

@@ -89,3 +89,7 @@ async fn mark_duplicates(ctx: &AppContext, rows: &mut [AnnotatedRow]) -> Result<
Ok(())
}
#[cfg(test)]
#[path = "tests/apply_mapping.rs"]
mod tests;

View File

@@ -27,3 +27,7 @@ pub async fn execute(ctx: &AppContext, cmd: ApplyImportProfileCommand) -> Result
session.row_results = None;
ctx.repos.import_session.update(&session).await
}
#[cfg(test)]
#[path = "tests/apply_profile.rs"]
mod tests;

View File

@@ -4,3 +4,7 @@ use domain::errors::DomainError;
pub async fn execute(ctx: &AppContext) -> Result<u64, DomainError> {
ctx.repos.import_session.delete_expired().await
}
#[cfg(test)]
#[path = "tests/cleanup.rs"]
mod tests;

View File

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

View File

@@ -15,3 +15,7 @@ pub async fn execute(ctx: &AppContext, cmd: DeleteImportProfileCommand) -> Resul
.ok_or_else(|| DomainError::NotFound("import profile".into()))?;
ctx.repos.import_profile.delete(&profile_id).await
}
#[cfg(test)]
#[path = "tests/delete_profile.rs"]
mod tests;

View File

@@ -9,7 +9,6 @@ use uuid::Uuid;
use crate::{
context::AppContext,
diary::commands::{LogReviewCommand, MovieInput},
diary::log_review,
import::commands::ExecuteImportCommand,
};
@@ -47,7 +46,7 @@ pub async fn execute(
}
match annotated.result {
RowResult::Valid(row) => match row_to_command(&row, user_id.value()) {
Ok(cmd) => match log_review::execute(ctx, cmd).await {
Ok(cmd) => match ctx.services.review_logger.log_review(cmd).await {
Ok(_) => imported += 1,
Err(e) => failed.push((idx, e.to_string())),
},
@@ -68,6 +67,10 @@ pub async fn execute(
})
}
#[cfg(test)]
#[path = "tests/execute.rs"]
mod tests;
fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result<LogReviewCommand, String> {
let rating = row
.rating

View File

@@ -7,3 +7,7 @@ pub async fn execute(
) -> Result<Vec<ImportProfile>, DomainError> {
ctx.repos.import_profile.list_for_user(user_id).await
}
#[cfg(test)]
#[path = "tests/list_profiles.rs"]
mod tests;

View File

@@ -33,3 +33,7 @@ pub async fn execute(
ctx.repos.import_profile.save(&profile).await?;
Ok(id)
}
#[cfg(test)]
#[path = "tests/save_profile.rs"]
mod tests;

View File

@@ -0,0 +1,208 @@
use std::sync::Arc;
use uuid::Uuid;
use domain::{
models::{
AnnotatedRow, Movie,
import::{ImportRow, ParsedFile, RowResult},
},
ports::{DocumentParser, MovieRepository},
testing::InMemoryMovieRepository,
value_objects::{ExternalMetadataId, MovieTitle, ReleaseYear},
};
use crate::import::{
apply_mapping,
commands::{ApplyImportMappingCommand, CreateImportSessionCommand},
create_session,
};
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn applies_mapping_to_session() {
let ctx = TestContextBuilder::new().build();
let user_id = Uuid::new_v4();
let session = create_session::execute(
&ctx,
CreateImportSessionCommand {
user_id,
bytes: b"title\nTest".to_vec(),
format: domain::models::FileFormat::Csv,
},
)
.await
.unwrap();
let rows = apply_mapping::execute(
&ctx,
ApplyImportMappingCommand {
user_id,
session_id: session.session_id.value(),
mappings: vec![],
},
)
.await
.unwrap();
assert!(!rows.is_empty());
}
#[tokio::test]
async fn fails_when_session_not_found() {
let ctx = TestContextBuilder::new().build();
let result = apply_mapping::execute(
&ctx,
ApplyImportMappingCommand {
user_id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
mappings: vec![],
},
)
.await;
assert!(result.is_err());
}
/// A document parser that returns rows with specific field values for testing
/// the mark_duplicates logic.
struct DuplicateTestParser {
rows: Vec<ImportRow>,
}
impl DocumentParser for DuplicateTestParser {
fn parse(
&self,
_: &[u8],
_: domain::models::FileFormat,
) -> Result<ParsedFile, domain::models::import::ImportError> {
Ok(ParsedFile {
columns: vec!["title".into()],
rows: vec![vec!["x".into()]],
})
}
fn apply_mapping(
&self,
_: &ParsedFile,
_: &[domain::models::FieldMapping],
) -> Vec<AnnotatedRow> {
self.rows
.iter()
.map(|r| AnnotatedRow {
result: RowResult::Valid(r.clone()),
is_duplicate: false,
})
.collect()
}
}
#[tokio::test]
async fn marks_duplicate_by_external_id() {
let movies = InMemoryMovieRepository::new();
let ext_id = ExternalMetadataId::new("tt1234567".into()).unwrap();
let movie = Movie::new(
Some(ext_id),
MovieTitle::new("Known Movie".into()).unwrap(),
ReleaseYear::new(2020).unwrap(),
None,
None,
);
movies.upsert_movie(&movie).await.unwrap();
let parser = DuplicateTestParser {
rows: vec![ImportRow {
title: Some("Known Movie".into()),
release_year: Some("2020".into()),
external_metadata_id: Some("tt1234567".into()),
..ImportRow::default()
}],
};
let ctx = TestContextBuilder::new()
.with_movies(Arc::clone(&movies) as _)
.with_document_parser(Arc::new(parser) as _)
.build();
let user_id = Uuid::new_v4();
let session = create_session::execute(
&ctx,
CreateImportSessionCommand {
user_id,
bytes: b"title\nKnown Movie".to_vec(),
format: domain::models::FileFormat::Csv,
},
)
.await
.unwrap();
let rows = apply_mapping::execute(
&ctx,
ApplyImportMappingCommand {
user_id,
session_id: session.session_id.value(),
mappings: vec![],
},
)
.await
.unwrap();
let has_dup = rows.iter().any(|r| r.is_duplicate);
assert!(has_dup, "row with matching external_id should be duplicate");
}
#[tokio::test]
async fn marks_duplicate_by_title_and_year() {
let movies = InMemoryMovieRepository::new();
let movie = Movie::new(
None,
MovieTitle::new("Duplicate Film".into()).unwrap(),
ReleaseYear::new(2022).unwrap(),
None,
None,
);
movies.upsert_movie(&movie).await.unwrap();
let parser = DuplicateTestParser {
rows: vec![ImportRow {
title: Some("Duplicate Film".into()),
release_year: Some("2022".into()),
..ImportRow::default()
}],
};
let ctx = TestContextBuilder::new()
.with_movies(Arc::clone(&movies) as _)
.with_document_parser(Arc::new(parser) as _)
.build();
let user_id = Uuid::new_v4();
let session = create_session::execute(
&ctx,
CreateImportSessionCommand {
user_id,
bytes: b"title\nDuplicate Film".to_vec(),
format: domain::models::FileFormat::Csv,
},
)
.await
.unwrap();
let rows = apply_mapping::execute(
&ctx,
ApplyImportMappingCommand {
user_id,
session_id: session.session_id.value(),
mappings: vec![],
},
)
.await
.unwrap();
let has_dup = rows.iter().any(|r| r.is_duplicate);
assert!(has_dup, "row with matching title+year should be duplicate");
}

View File

@@ -0,0 +1,115 @@
use std::sync::Arc;
use chrono::Utc;
use domain::models::ImportProfile;
use domain::ports::{ImportProfileRepository, ImportSessionRepository};
use domain::testing::InMemoryImportProfileRepository;
use domain::value_objects::{ImportProfileId, UserId};
use uuid::Uuid;
use crate::import::{apply_profile, commands::ApplyImportProfileCommand};
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn fails_when_profile_not_found() {
let ctx = TestContextBuilder::new().build();
let result = apply_profile::execute(
&ctx,
ApplyImportProfileCommand {
user_id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
profile_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn fails_when_session_not_found() {
let profiles = InMemoryImportProfileRepository::new();
let user_id = Uuid::new_v4();
let profile = ImportProfile::new(
ImportProfileId::generate(),
UserId::from_uuid(user_id),
"test".into(),
vec![],
Utc::now().naive_utc(),
);
let profile_id = profile.id.clone();
profiles.save(&profile).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_profiles(Arc::clone(&profiles) as _)
.build();
let result = apply_profile::execute(
&ctx,
ApplyImportProfileCommand {
user_id,
session_id: Uuid::new_v4(),
profile_id: profile_id.value(),
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn applies_profile_mappings_to_session() {
let profiles = InMemoryImportProfileRepository::new();
let sessions = domain::testing::InMemoryImportSessionRepository::new();
let user_id = Uuid::new_v4();
let profile = ImportProfile::new(
ImportProfileId::generate(),
UserId::from_uuid(user_id),
"letterboxd".into(),
vec![domain::models::FieldMapping {
source_column: "Name".into(),
domain_field: domain::models::import::DomainField::Title,
transform: domain::models::import::Transform::Identity,
}],
Utc::now().naive_utc(),
);
let profile_id = profile.id.clone();
profiles.save(&profile).await.unwrap();
let session = domain::models::ImportSession::new(
domain::value_objects::ImportSessionId::generate(),
UserId::from_uuid(user_id),
Utc::now().naive_utc(),
);
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_profiles(Arc::clone(&profiles) as _)
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
apply_profile::execute(
&ctx,
ApplyImportProfileCommand {
user_id,
session_id: session_id.value(),
profile_id: profile_id.value(),
},
)
.await
.unwrap();
// Verify the session got updated with field_mappings and row_results cleared
let updated = sessions
.get(&session_id, &UserId::from_uuid(user_id))
.await
.unwrap()
.unwrap();
assert!(updated.field_mappings.is_some());
assert_eq!(updated.field_mappings.unwrap().len(), 1);
assert!(updated.row_results.is_none());
}

View File

@@ -0,0 +1,18 @@
use std::sync::Arc;
use domain::testing::InMemoryImportSessionRepository;
use crate::import::cleanup;
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn returns_zero_when_nothing_expired() {
let sessions = InMemoryImportSessionRepository::new();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = cleanup::execute(&ctx).await.unwrap();
assert_eq!(result, 0);
}

View File

@@ -0,0 +1,22 @@
use uuid::Uuid;
use crate::import::{commands::CreateImportSessionCommand, create_session};
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn creates_session_with_parsed_file() {
let ctx = TestContextBuilder::new().build();
let result = create_session::execute(
&ctx,
CreateImportSessionCommand {
user_id: Uuid::new_v4(),
bytes: b"col1\nval1".to_vec(),
format: domain::models::FileFormat::Csv,
},
)
.await
.unwrap();
assert!(!result.columns.is_empty());
}

View File

@@ -0,0 +1,26 @@
use std::sync::Arc;
use domain::testing::InMemoryImportProfileRepository;
use uuid::Uuid;
use crate::import::{commands::DeleteImportProfileCommand, delete_profile};
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn fails_when_profile_not_found() {
let profiles = InMemoryImportProfileRepository::new();
let ctx = TestContextBuilder::new()
.with_import_profiles(Arc::clone(&profiles) as _)
.build();
let result = delete_profile::execute(
&ctx,
DeleteImportProfileCommand {
user_id: Uuid::new_v4(),
profile_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
}

View File

@@ -0,0 +1,558 @@
use std::sync::Arc;
use chrono::Utc;
use domain::models::{AnnotatedRow, ImportSession, import::RowResult};
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryImportSessionRepository;
use domain::value_objects::{ImportSessionId, UserId};
use uuid::Uuid;
use crate::import::commands::ExecuteImportCommand;
use crate::import::execute;
use crate::test_helpers::TestContextBuilder;
fn make_session_with_rows(user_id: UserId, session_id: ImportSessionId) -> ImportSession {
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(session_id, user_id, now);
session.row_results = Some(vec![
AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("Test Movie".into()),
release_year: Some("2024".into()),
rating: Some("4".into()),
watched_at: Some("2024-06-01".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
},
AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("Another".into()),
release_year: Some("2023".into()),
rating: Some("3".into()),
watched_at: Some("2024-07-01".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
},
]);
session
}
#[tokio::test]
async fn imports_confirmed_rows() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let session = make_session_with_rows(UserId::from_uuid(uid), sid.clone());
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0, 1],
},
)
.await
.unwrap();
assert_eq!(result.imported, 2);
assert_eq!(result.skipped_duplicates, 0);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn skips_unconfirmed_rows() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let session = make_session_with_rows(UserId::from_uuid(uid), sid.clone());
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 1);
assert_eq!(result.skipped_duplicates, 1);
}
#[tokio::test]
async fn fails_when_session_not_found() {
let ctx = TestContextBuilder::new().build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
confirmed_indices: vec![],
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn handles_datetime_format() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("DateTime Movie".into()),
release_year: Some("2024".into()),
rating: Some("5".into()),
watched_at: Some("2024-06-01T12:30:00".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn fails_on_invalid_rating() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("Bad Rating Movie".into()),
release_year: Some("2024".into()),
rating: Some("not_a_number".into()),
watched_at: Some("2024-06-01".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 0);
assert_eq!(result.failed.len(), 1);
}
#[tokio::test]
async fn fails_on_missing_watched_at() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("No Date Movie".into()),
release_year: Some("2024".into()),
rating: Some("4".into()),
watched_at: None,
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 0);
assert_eq!(result.failed.len(), 1);
}
#[tokio::test]
async fn imports_row_with_external_metadata_id() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("TMDB Movie".into()),
release_year: Some("2024".into()),
rating: Some("5".into()),
watched_at: Some("2024-06-01".into()),
external_metadata_id: Some("tt9999999".into()),
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn imports_row_with_director_and_comment() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("Directed Movie".into()),
release_year: Some("2022".into()),
rating: Some("4".into()),
watched_at: Some("2024-06-01".into()),
external_metadata_id: None,
director: Some("John Director".into()),
comment: Some("A great film".into()),
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn handles_space_separated_datetime_format() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("Space DateTime".into()),
release_year: Some("2024".into()),
rating: Some("3".into()),
watched_at: Some("2024-06-01 14:30:00".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn reports_invalid_row_result_errors() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Invalid {
errors: vec!["missing title".into(), "bad year".into()],
raw: vec![("col1".into(), "val1".into())],
},
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 0);
assert_eq!(result.failed.len(), 1);
assert!(result.failed[0].1.contains("missing title"));
assert!(result.failed[0].1.contains("bad year"));
}
#[tokio::test]
async fn fails_on_missing_rating() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("No Rating Movie".into()),
release_year: Some("2024".into()),
rating: None,
watched_at: Some("2024-06-01".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 0);
assert_eq!(result.failed.len(), 1);
assert!(result.failed[0].1.contains("missing rating"));
}
#[tokio::test]
async fn fails_on_unparseable_date() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("Bad Date Movie".into()),
release_year: Some("2024".into()),
rating: Some("3".into()),
watched_at: Some("not-a-date".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 0);
assert_eq!(result.failed.len(), 1);
assert!(result.failed[0].1.contains("cannot parse watched_at"));
}
#[tokio::test]
async fn imports_row_without_release_year() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let now = Utc::now().naive_utc();
let mut session = ImportSession::new(sid.clone(), UserId::from_uuid(uid), now);
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some("No Year Movie".into()),
release_year: None,
rating: Some("4".into()),
watched_at: Some("2024-06-01".into()),
external_metadata_id: None,
director: None,
comment: None,
}),
is_duplicate: false,
}]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn deletes_session_after_import() {
let sessions = InMemoryImportSessionRepository::new();
let uid = Uuid::new_v4();
let sid = ImportSessionId::generate();
let session = make_session_with_rows(UserId::from_uuid(uid), sid.clone());
sessions.create(&session).await.unwrap();
assert_eq!(sessions.count(), 1);
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
execute::execute(
&ctx,
ExecuteImportCommand {
user_id: uid,
session_id: sid.value(),
confirmed_indices: vec![0],
},
)
.await
.unwrap();
assert_eq!(
sessions.count(),
0,
"session should be deleted after import"
);
}

View File

@@ -0,0 +1,21 @@
use std::sync::Arc;
use domain::testing::InMemoryImportProfileRepository;
use domain::value_objects::UserId;
use uuid::Uuid;
use crate::import::list_profiles;
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn returns_empty_when_no_profiles() {
let profiles = InMemoryImportProfileRepository::new();
let ctx = TestContextBuilder::new()
.with_import_profiles(Arc::clone(&profiles) as _)
.build();
let user_id = UserId::from_uuid(Uuid::new_v4());
let result = list_profiles::execute(&ctx, &user_id).await.unwrap();
assert!(result.is_empty());
}

View File

@@ -0,0 +1,62 @@
use std::sync::Arc;
use chrono::Utc;
use domain::models::ImportSession;
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryImportSessionRepository;
use domain::value_objects::{ImportSessionId, UserId};
use uuid::Uuid;
use crate::import::{commands::SaveImportProfileCommand, save_profile};
use crate::test_helpers::TestContextBuilder;
#[tokio::test]
async fn fails_when_session_not_found() {
let sessions = InMemoryImportSessionRepository::new();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = save_profile::execute(
&ctx,
SaveImportProfileCommand {
user_id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
name: "my profile".into(),
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn saves_profile_from_session() {
let sessions = InMemoryImportSessionRepository::new();
let user_id = Uuid::new_v4();
let sid = ImportSessionId::generate();
let mut session = ImportSession::new(
sid.clone(),
UserId::from_uuid(user_id),
Utc::now().naive_utc(),
);
session.field_mappings = Some(vec![]);
sessions.create(&session).await.unwrap();
let ctx = TestContextBuilder::new()
.with_import_sessions(Arc::clone(&sessions) as _)
.build();
let result = save_profile::execute(
&ctx,
SaveImportProfileCommand {
user_id,
session_id: sid.value(),
name: "my profile".into(),
},
)
.await;
assert!(result.is_ok());
}