test: move application tests to tests/

test_support/test_helpers -> tests/support; watched_at unified w/ domain test_watched_at (2024-03-15).
Kept inline: users::diary_filter tests (feed_sort_to_direction_all_variants, paged_user_filter_builds_correctly) - pub(super) helpers in private module.
Dropped stale never-compiled users/tests/get_users.rs (old execute signature).
This commit is contained in:
2026-10-25 22:36:52 +01:00
parent a2c713c1fa
commit 8526b77f18
175 changed files with 541 additions and 695 deletions

View File

@@ -0,0 +1,200 @@
use crate::support::{apply_import_mapping_command, create_import_session_command};
use domain::testing::{test_movie_with_external_id, test_movie_with_year};
use std::sync::Arc;
use uuid::Uuid;
use domain::{
models::{
AnnotatedRow,
import::{ImportRow, ParsedFile, RowResult},
},
ports::{DocumentParser, MovieCommand},
testing::InMemoryStore,
value_objects::ExternalMetadataId,
};
use crate::support::TestContextBuilder;
use application::import::deps::{ApplyMappingDeps, CreateSessionDeps};
use application::import::{apply_mapping, create_session};
#[tokio::test]
async fn applies_mapping_to_session() {
let sessions = InMemoryStore::new();
let b = TestContextBuilder::new();
let user_id = Uuid::new_v4();
let create_deps = CreateSessionDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
};
let session = create_session::execute(
&create_deps,
create_import_session_command(user_id, b"title\nTest"),
)
.await
.unwrap();
let mapping_deps = ApplyMappingDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
movie_query: b.movie_query.clone(),
};
let rows = apply_mapping::execute(
&mapping_deps,
apply_import_mapping_command(user_id, session.session_id.value()),
)
.await
.unwrap();
assert!(!rows.is_empty());
}
#[tokio::test]
async fn fails_when_session_not_found() {
let sessions = InMemoryStore::new();
let b = TestContextBuilder::new();
let deps = ApplyMappingDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
movie_query: b.movie_query.clone(),
};
let result = apply_mapping::execute(
&deps,
apply_import_mapping_command(Uuid::new_v4(), Uuid::new_v4()),
)
.await;
assert!(result.is_err());
}
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 = InMemoryStore::new();
let sessions = Arc::clone(&movies);
let ext_id = ExternalMetadataId::new("tt1234567".into()).unwrap();
let movie = test_movie_with_external_id("Known Movie", 2020, ext_id);
movies.upsert_movie(&movie).await.unwrap();
let parser = Arc::new(DuplicateTestParser {
rows: vec![ImportRow {
title: Some("Known Movie".into()),
release_year: Some("2020".into()),
external_metadata_id: Some("tt1234567".into()),
..ImportRow::default()
}],
});
let user_id = Uuid::new_v4();
let create_deps = CreateSessionDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: Arc::clone(&parser) as _,
};
let session = create_session::execute(
&create_deps,
create_import_session_command(user_id, b"title\nKnown Movie"),
)
.await
.unwrap();
let mapping_deps = ApplyMappingDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: Arc::clone(&parser) as _,
movie_query: Arc::clone(&movies) as _,
};
let rows = apply_mapping::execute(
&mapping_deps,
apply_import_mapping_command(user_id, session.session_id.value()),
)
.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 = InMemoryStore::new();
let sessions = Arc::clone(&movies);
let movie = test_movie_with_year("Duplicate Film", 2022);
movies.upsert_movie(&movie).await.unwrap();
let parser = Arc::new(DuplicateTestParser {
rows: vec![ImportRow {
title: Some("Duplicate Film".into()),
release_year: Some("2022".into()),
..ImportRow::default()
}],
});
let user_id = Uuid::new_v4();
let create_deps = CreateSessionDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: Arc::clone(&parser) as _,
};
let session = create_session::execute(
&create_deps,
create_import_session_command(user_id, b"title\nDuplicate Film"),
)
.await
.unwrap();
let mapping_deps = ApplyMappingDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: Arc::clone(&parser) as _,
movie_query: Arc::clone(&movies) as _,
};
let rows = apply_mapping::execute(
&mapping_deps,
apply_import_mapping_command(user_id, session.session_id.value()),
)
.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,104 @@
use crate::support::create_import_session_command;
use std::sync::Arc;
use chrono::Utc;
use uuid::Uuid;
use domain::models::import::{DomainField, Transform};
use domain::models::{FieldMapping, ImportProfile};
use domain::ports::{ImportProfileRepository, ImportSessionRepository};
use domain::testing::InMemoryStore;
use domain::value_objects::{ImportProfileId, UserId};
use crate::support::TestContextBuilder;
use application::import::deps::{ApplyProfileAndMapDeps, CreateSessionDeps};
use application::import::{
apply_profile_and_map, commands::ApplyProfileAndMapCommand, create_session,
};
#[tokio::test]
async fn fails_when_profile_not_found() {
let profiles = InMemoryStore::new();
let sessions = Arc::clone(&profiles);
let b = TestContextBuilder::new();
let deps = ApplyProfileAndMapDeps {
import_profile: Arc::clone(&profiles) as _,
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
movie_query: b.movie_query.clone(),
};
let result = apply_profile_and_map::execute(
&deps,
ApplyProfileAndMapCommand {
user_id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
profile_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn applies_profile_then_regenerates_mapping() {
let profiles = InMemoryStore::new();
let sessions = Arc::clone(&profiles);
let b = TestContextBuilder::new();
let user_id = Uuid::new_v4();
let profile = ImportProfile::new(
ImportProfileId::generate(),
UserId::from_uuid(user_id),
"letterboxd".into(),
vec![FieldMapping {
source_column: "title".into(),
domain_field: DomainField::Title,
transform: Transform::Identity,
}],
Utc::now().naive_utc(),
);
let profile_id = profile.id.clone();
profiles.save(&profile).await.unwrap();
let create_deps = CreateSessionDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
};
let created = create_session::execute(
&create_deps,
create_import_session_command(user_id, b"title\nTest"),
)
.await
.unwrap();
let deps = ApplyProfileAndMapDeps {
import_profile: Arc::clone(&profiles) as _,
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
movie_query: b.movie_query.clone(),
};
let rows = apply_profile_and_map::execute(
&deps,
ApplyProfileAndMapCommand {
user_id,
session_id: created.session_id.value(),
profile_id: profile_id.value(),
},
)
.await
.unwrap();
assert!(!rows.is_empty());
let updated =
ImportSessionRepository::get(&*sessions, &created.session_id, &UserId::from_uuid(user_id))
.await
.unwrap()
.unwrap();
assert!(updated.row_results.is_some());
assert!(updated.field_mappings.is_some());
}

View File

@@ -0,0 +1,108 @@
use crate::support::apply_import_profile_command;
use std::sync::Arc;
use chrono::Utc;
use domain::models::ImportProfile;
use domain::ports::{ImportProfileRepository, ImportSessionRepository};
use domain::testing::InMemoryStore;
use domain::value_objects::{ImportProfileId, UserId};
use uuid::Uuid;
use application::import::apply_profile;
use application::import::deps::ApplyProfileDeps;
#[tokio::test]
async fn fails_when_profile_not_found() {
let profiles = InMemoryStore::new();
let sessions = Arc::clone(&profiles);
let deps = ApplyProfileDeps {
import_profile: Arc::clone(&profiles) as _,
import_session: Arc::clone(&sessions) as _,
};
let result = apply_profile::execute(
&deps,
apply_import_profile_command(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn fails_when_session_not_found() {
let profiles = InMemoryStore::new();
let sessions = Arc::clone(&profiles);
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 deps = ApplyProfileDeps {
import_profile: Arc::clone(&profiles) as _,
import_session: Arc::clone(&sessions) as _,
};
let result = apply_profile::execute(
&deps,
apply_import_profile_command(user_id, Uuid::new_v4(), profile_id.value()),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn applies_profile_mappings_to_session() {
let profiles = InMemoryStore::new();
let sessions = domain::testing::InMemoryStore::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(UserId::from_uuid(user_id));
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = ApplyProfileDeps {
import_profile: Arc::clone(&profiles) as _,
import_session: Arc::clone(&sessions) as _,
};
apply_profile::execute(
&deps,
apply_import_profile_command(user_id, session_id.value(), profile_id.value()),
)
.await
.unwrap();
// Verify the session got updated with field_mappings and row_results cleared
let updated =
ImportSessionRepository::get(&*sessions, &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,14 @@
use std::sync::Arc;
use domain::testing::InMemoryStore;
use application::import::cleanup;
#[tokio::test]
async fn returns_zero_when_nothing_expired() {
let sessions = InMemoryStore::new();
let result = cleanup::execute(Arc::clone(&sessions) as _).await.unwrap();
assert_eq!(result, 0);
}

View File

@@ -0,0 +1,30 @@
use crate::support::create_import_session_command;
use std::sync::Arc;
use uuid::Uuid;
use domain::testing::InMemoryStore;
use crate::support::TestContextBuilder;
use application::import::create_session;
use application::import::deps::CreateSessionDeps;
#[tokio::test]
async fn creates_session_with_parsed_file() {
let sessions = InMemoryStore::new();
let b = TestContextBuilder::new();
let deps = CreateSessionDeps {
import_session: Arc::clone(&sessions) as _,
document_parser: b.document_parser.clone(),
};
let result = create_session::execute(
&deps,
create_import_session_command(Uuid::new_v4(), b"col1\nval1"),
)
.await
.unwrap();
assert!(!result.columns.is_empty());
}

View File

@@ -0,0 +1,27 @@
use std::sync::Arc;
use domain::testing::InMemoryStore;
use uuid::Uuid;
use application::import::{
commands::DeleteImportProfileCommand, delete_profile, deps::DeleteImportProfileDeps,
};
#[tokio::test]
async fn fails_when_profile_not_found() {
let profiles = InMemoryStore::new();
let deps = DeleteImportProfileDeps {
import_profile: Arc::clone(&profiles) as _,
};
let result = delete_profile::execute(
&deps,
DeleteImportProfileCommand {
user_id: Uuid::new_v4(),
profile_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
}

View File

@@ -0,0 +1,527 @@
use crate::support::execute_import_command;
use std::sync::Arc;
use domain::models::{AnnotatedRow, ImportSession, import::RowResult};
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryStore;
use domain::value_objects::UserId;
use uuid::Uuid;
use crate::support::NoopReviewLogger;
use application::import::commands::ExecuteImportCommand;
use application::import::deps::ExecuteImportDeps;
use application::import::execute;
fn make_session_with_rows(user_id: UserId) -> ImportSession {
let mut session = ImportSession::new(user_id);
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 = InMemoryStore::new();
let uid = Uuid::new_v4();
let session = make_session_with_rows(UserId::from_uuid(uid));
let sid = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(
&deps,
ExecuteImportCommand {
confirmed_indices: vec![0, 1],
..execute_import_command(uid, sid.value())
},
)
.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 = InMemoryStore::new();
let uid = Uuid::new_v4();
let session = make_session_with_rows(UserId::from_uuid(uid));
let sid = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 1);
assert_eq!(result.skipped_duplicates, 1);
}
#[tokio::test]
async fn fails_when_session_not_found() {
let sessions = InMemoryStore::new();
let deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(
&deps,
ExecuteImportCommand {
confirmed_indices: vec![],
..execute_import_command(Uuid::new_v4(), Uuid::new_v4())
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn handles_datetime_format() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn fails_on_invalid_rating() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 0);
assert_eq!(result.failed.len(), 1);
}
#[tokio::test]
async fn fails_on_missing_watched_at() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.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 = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn imports_row_with_director_and_comment() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn handles_space_separated_datetime_format() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn reports_invalid_row_result_errors() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.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 = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.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 = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.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 = InMemoryStore::new();
let uid = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
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 deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let result = execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(result.imported, 1);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn deletes_session_after_import() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let session = make_session_with_rows(UserId::from_uuid(uid));
let sid = session.id.clone();
sessions.create(&session).await.unwrap();
assert_eq!(sessions.import_session_count(), 1);
let deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
execute::execute(&deps, execute_import_command(uid, sid.value()))
.await
.unwrap();
assert_eq!(
sessions.import_session_count(),
0,
"session should be deleted after import"
);
}
#[tokio::test]
async fn imports_more_rows_than_concurrency_limit() {
let sessions = InMemoryStore::new();
let uid = Uuid::new_v4();
let rows: Vec<_> = (0..15)
.map(|i| AnnotatedRow {
result: RowResult::Valid(domain::models::ImportRow {
title: Some(format!("Movie {i}")),
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,
})
.collect();
let mut session = ImportSession::new(UserId::from_uuid(uid));
let sid = session.id.clone();
session.row_results = Some(rows);
sessions.create(&session).await.unwrap();
let deps = ExecuteImportDeps {
import_session: Arc::clone(&sessions) as _,
review_logger: Arc::new(NoopReviewLogger),
};
let confirmed_indices: Vec<usize> = (0..15).collect();
let result = execute::execute(
&deps,
ExecuteImportCommand {
confirmed_indices,
..execute_import_command(uid, sid.value())
},
)
.await
.unwrap();
assert_eq!(result.imported, 15);
assert_eq!(result.skipped_duplicates, 0);
assert!(result.failed.is_empty());
}

View File

@@ -0,0 +1,68 @@
use std::sync::Arc;
use uuid::Uuid;
use domain::models::ImportSession;
use domain::models::import::ParsedFile;
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryStore;
use domain::value_objects::{ImportSessionId, UserId};
use application::import::deps::GetMappingStageDeps;
use application::import::get_mapping_stage::{self, SAMPLE_ROW_LIMIT};
#[tokio::test]
async fn get_mapping_stage_is_not_found_when_file_not_parsed() {
let sessions = InMemoryStore::new();
let user_id = Uuid::new_v4();
let session = ImportSession::new(UserId::from_uuid(user_id));
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = GetMappingStageDeps {
import_session: Arc::clone(&sessions) as _,
};
let result = get_mapping_stage::execute(&deps, session_id, user_id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_mapping_stage_is_not_found_when_session_missing() {
let sessions = InMemoryStore::new();
let deps = GetMappingStageDeps {
import_session: Arc::clone(&sessions) as _,
};
let result =
get_mapping_stage::execute(&deps, ImportSessionId::generate(), Uuid::new_v4()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_mapping_stage_returns_columns_and_capped_sample_rows() {
let sessions = InMemoryStore::new();
let user_id = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(user_id));
session.parsed_file = Some(ParsedFile {
columns: vec!["Name".into(), "Year".into()],
rows: (0..7)
.map(|i| vec![format!("row{i}"), "2020".into()])
.collect(),
});
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = GetMappingStageDeps {
import_session: Arc::clone(&sessions) as _,
};
let stage = get_mapping_stage::execute(&deps, session_id, user_id)
.await
.unwrap();
assert_eq!(stage.columns, vec!["Name".to_string(), "Year".to_string()]);
assert_eq!(stage.sample_rows.len(), SAMPLE_ROW_LIMIT);
}

View File

@@ -0,0 +1,80 @@
use std::sync::Arc;
use uuid::Uuid;
use domain::models::import::{ImportRow, ParsedFile, RowResult};
use domain::models::{AnnotatedRow, ImportSession};
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryStore;
use domain::value_objects::{ImportSessionId, UserId};
use application::import::deps::GetPreviewStageDeps;
use application::import::get_preview_stage::{self, PreviewStage};
#[tokio::test]
async fn get_preview_stage_reports_not_yet_mapped_when_row_results_absent() {
let sessions = InMemoryStore::new();
let user_id = Uuid::new_v4();
let session = ImportSession::new(UserId::from_uuid(user_id));
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = GetPreviewStageDeps {
import_session: Arc::clone(&sessions) as _,
};
let stage = get_preview_stage::execute(&deps, session_id, user_id)
.await
.unwrap();
assert!(matches!(stage, PreviewStage::NotYetMapped));
}
#[tokio::test]
async fn get_preview_stage_returns_rows_once_mapped() {
let sessions = InMemoryStore::new();
let user_id = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(user_id));
session.parsed_file = Some(ParsedFile {
columns: vec!["Name".into()],
rows: vec![vec!["Test".into()]],
});
session.row_results = Some(vec![AnnotatedRow {
result: RowResult::Valid(ImportRow {
title: Some("Test".into()),
..ImportRow::default()
}),
is_duplicate: false,
}]);
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = GetPreviewStageDeps {
import_session: Arc::clone(&sessions) as _,
};
let stage = get_preview_stage::execute(&deps, session_id, user_id)
.await
.unwrap();
match stage {
PreviewStage::Ready(preview) => {
assert_eq!(preview.columns, vec!["Name".to_string()]);
assert_eq!(preview.rows.len(), 1);
}
PreviewStage::NotYetMapped => panic!("expected Ready, got NotYetMapped"),
}
}
#[tokio::test]
async fn get_preview_stage_is_not_found_when_session_missing() {
let sessions = InMemoryStore::new();
let deps = GetPreviewStageDeps {
import_session: Arc::clone(&sessions) as _,
};
let result =
get_preview_stage::execute(&deps, ImportSessionId::generate(), Uuid::new_v4()).await;
assert!(result.is_err());
}

View File

@@ -0,0 +1,50 @@
use std::sync::Arc;
use uuid::Uuid;
use domain::models::ImportSession;
use domain::models::import::ParsedFile;
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryStore;
use domain::value_objects::{ImportSessionId, UserId};
use application::import::deps::GetSessionStateDeps;
use application::import::get_session_state;
#[tokio::test]
async fn get_session_state_is_not_found_when_session_missing() {
let sessions = InMemoryStore::new();
let deps = GetSessionStateDeps {
import_session: Arc::clone(&sessions) as _,
};
let result =
get_session_state::execute(&deps, ImportSessionId::generate(), Uuid::new_v4()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_session_state_reports_columns_row_count_and_mapping_status() {
let sessions = InMemoryStore::new();
let user_id = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(user_id));
session.parsed_file = Some(ParsedFile {
columns: vec!["Name".into()],
rows: vec![vec!["a".into()], vec!["b".into()]],
});
let session_id = session.id.clone();
sessions.create(&session).await.unwrap();
let deps = GetSessionStateDeps {
import_session: Arc::clone(&sessions) as _,
};
let state = get_session_state::execute(&deps, session_id, user_id)
.await
.unwrap();
assert_eq!(state.columns, vec!["Name".to_string()]);
assert_eq!(state.row_count, 2);
assert!(!state.has_mappings);
}

View File

@@ -0,0 +1,20 @@
use std::sync::Arc;
use domain::testing::InMemoryStore;
use domain::value_objects::UserId;
use uuid::Uuid;
use application::import::{deps::ListImportProfilesDeps, list_profiles};
#[tokio::test]
async fn returns_empty_when_no_profiles() {
let profiles = InMemoryStore::new();
let deps = ListImportProfilesDeps {
import_profile: Arc::clone(&profiles) as _,
};
let user_id = UserId::from_uuid(Uuid::new_v4());
let result = list_profiles::execute(&deps, &user_id).await.unwrap();
assert!(result.is_empty());
}

View File

@@ -0,0 +1,62 @@
use std::sync::Arc;
use domain::models::ImportSession;
use domain::ports::ImportSessionRepository;
use domain::testing::InMemoryStore;
use domain::value_objects::UserId;
use uuid::Uuid;
use application::import::deps::SaveProfileDeps;
use application::import::{commands::SaveImportProfileCommand, save_profile};
#[tokio::test]
async fn fails_when_session_not_found() {
let sessions = InMemoryStore::new();
let profiles = Arc::clone(&sessions);
let deps = SaveProfileDeps {
import_session: Arc::clone(&sessions) as _,
import_profile: Arc::clone(&profiles) as _,
};
let result = save_profile::execute(
&deps,
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 = InMemoryStore::new();
let profiles = Arc::clone(&sessions);
let user_id = Uuid::new_v4();
let mut session = ImportSession::new(UserId::from_uuid(user_id));
let sid = session.id.clone();
session.field_mappings = Some(vec![]);
sessions.create(&session).await.unwrap();
let deps = SaveProfileDeps {
import_session: Arc::clone(&sessions) as _,
import_profile: Arc::clone(&profiles) as _,
};
let result = save_profile::execute(
&deps,
SaveImportProfileCommand {
user_id,
session_id: sid.value(),
name: "my profile".into(),
},
)
.await;
assert!(result.is_ok());
}