structural refactor and codebase improvements
This commit is contained in:
64
crates/application/src/import/apply_profile_and_map.rs
Normal file
64
crates/application/src/import/apply_profile_and_map.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! Absorbs `handlers/import.rs::api_apply_profile`'s three-step orchestration:
|
||||
//! apply the saved profile's field mappings onto the session, reload the
|
||||
//! session to read back the mappings `apply_profile` just wrote, then run
|
||||
//! `apply_mapping` to regenerate `row_results` from them. All three steps used
|
||||
//! to live in the handler; this use case is the only caller-visible change —
|
||||
//! the two existing use cases it drives (`apply_profile::execute`,
|
||||
//! `apply_mapping::execute`) are untouched, per this task's constraint against
|
||||
//! reshaping already-existing use-case signatures.
|
||||
|
||||
use domain::{errors::DomainError, value_objects::ImportSessionId};
|
||||
|
||||
use crate::import::{
|
||||
apply_mapping, apply_profile,
|
||||
commands::{ApplyImportMappingCommand, ApplyImportProfileCommand, ApplyProfileAndMapCommand},
|
||||
deps::{ApplyMappingDeps, ApplyProfileAndMapDeps, ApplyProfileDeps},
|
||||
};
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ApplyProfileAndMapDeps,
|
||||
cmd: ApplyProfileAndMapCommand,
|
||||
) -> Result<Vec<domain::models::AnnotatedRow>, DomainError> {
|
||||
let profile_deps = ApplyProfileDeps {
|
||||
import_profile: deps.import_profile.clone(),
|
||||
import_session: deps.import_session.clone(),
|
||||
};
|
||||
apply_profile::execute(
|
||||
&profile_deps,
|
||||
ApplyImportProfileCommand {
|
||||
user_id: cmd.user_id,
|
||||
session_id: cmd.session_id,
|
||||
profile_id: cmd.profile_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||
let user_id = domain::value_objects::UserId::from_uuid(cmd.user_id);
|
||||
let session = deps
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("session not found after profile apply".into()))?;
|
||||
|
||||
let mappings = session.field_mappings.unwrap_or_default();
|
||||
|
||||
let mapping_deps = ApplyMappingDeps {
|
||||
import_session: deps.import_session.clone(),
|
||||
document_parser: deps.document_parser.clone(),
|
||||
movie_query: deps.movie_query.clone(),
|
||||
};
|
||||
apply_mapping::execute(
|
||||
&mapping_deps,
|
||||
ApplyImportMappingCommand {
|
||||
user_id: cmd.user_id,
|
||||
session_id: cmd.session_id,
|
||||
mappings,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/apply_profile_and_map.rs"]
|
||||
mod tests;
|
||||
@@ -31,6 +31,12 @@ pub struct ApplyImportProfileCommand {
|
||||
pub profile_id: Uuid,
|
||||
}
|
||||
|
||||
pub struct ApplyProfileAndMapCommand {
|
||||
pub user_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub profile_id: Uuid,
|
||||
}
|
||||
|
||||
pub struct DeleteImportProfileCommand {
|
||||
pub user_id: Uuid,
|
||||
pub profile_id: Uuid,
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::import::commands::DeleteImportProfileCommand;
|
||||
use crate::import::deps::DeleteImportProfileDeps;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::ImportProfileRepository,
|
||||
value_objects::{ImportProfileId, UserId},
|
||||
};
|
||||
|
||||
pub async fn execute(
|
||||
import_profile: Arc<dyn ImportProfileRepository>,
|
||||
deps: &DeleteImportProfileDeps,
|
||||
cmd: DeleteImportProfileCommand,
|
||||
) -> Result<(), DomainError> {
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
let profile_id = ImportProfileId::from_uuid(cmd.profile_id);
|
||||
|
||||
import_profile
|
||||
deps.import_profile
|
||||
.get(&profile_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import profile".into()))?;
|
||||
import_profile.delete(&profile_id).await
|
||||
deps.import_profile.delete(&profile_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -29,3 +29,35 @@ pub struct SaveProfileDeps {
|
||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||
}
|
||||
|
||||
pub struct GetMappingStageDeps {
|
||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||
}
|
||||
|
||||
pub struct GetPreviewStageDeps {
|
||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||
}
|
||||
|
||||
pub struct GetSessionStateDeps {
|
||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||
}
|
||||
|
||||
pub struct DeleteImportProfileDeps {
|
||||
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||
}
|
||||
|
||||
pub struct ListImportProfilesDeps {
|
||||
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||
}
|
||||
|
||||
/// Backs `apply_profile_and_map`, which internally drives `apply_profile::execute`
|
||||
/// then `apply_mapping::execute` — these fields are exactly the union of
|
||||
/// `ApplyProfileDeps` and `ApplyMappingDeps`'s fields, cloned once here and used to
|
||||
/// build each nested deps struct inline at the call site (see that file's doc
|
||||
/// comment for why: no use-case signature changes, per this task's constraints).
|
||||
pub struct ApplyProfileAndMapDeps {
|
||||
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||
pub document_parser: Arc<dyn DocumentParser>,
|
||||
pub movie_query: Arc<dyn MovieQuery>,
|
||||
}
|
||||
|
||||
47
crates/application/src/import/get_mapping_stage.rs
Normal file
47
crates/application/src/import/get_mapping_stage.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! The mapping-page stage gate: a session must exist and have a `parsed_file`
|
||||
//! before its columns/sample rows can be shown for field mapping. Absorbs
|
||||
//! `handlers/import.rs::get_mapping_page`'s two early-return checks (session
|
||||
//! missing, `parsed_file` absent) — both collapse to `NotFound` here since the
|
||||
//! handler redirected to the same place (`/import`) for either.
|
||||
|
||||
use domain::{errors::DomainError, value_objects::ImportSessionId};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::import::deps::GetMappingStageDeps;
|
||||
|
||||
/// Cap on sample rows shown on the mapping page — was a bare `.take(5)` in the
|
||||
/// handler.
|
||||
pub const SAMPLE_ROW_LIMIT: usize = 5;
|
||||
|
||||
pub struct MappingStage {
|
||||
pub columns: Vec<String>,
|
||||
pub sample_rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
deps: &GetMappingStageDeps,
|
||||
session_id: ImportSessionId,
|
||||
user_id: Uuid,
|
||||
) -> Result<MappingStage, DomainError> {
|
||||
let user_id = domain::value_objects::UserId::from_uuid(user_id);
|
||||
let session = deps
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||
|
||||
let parsed = session
|
||||
.parsed_file
|
||||
.ok_or_else(|| DomainError::NotFound("import session has no parsed file".into()))?;
|
||||
|
||||
let sample_rows = parsed.rows.into_iter().take(SAMPLE_ROW_LIMIT).collect();
|
||||
|
||||
Ok(MappingStage {
|
||||
columns: parsed.columns,
|
||||
sample_rows,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_mapping_stage.rs"]
|
||||
mod tests;
|
||||
59
crates/application/src/import/get_preview_stage.rs
Normal file
59
crates/application/src/import/get_preview_stage.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! The preview-page stage gate: a session must have `row_results` (i.e. a
|
||||
//! mapping has already been applied) before its rows can be previewed. Serves
|
||||
//! both the HTML preview handler and the API preview handler —
|
||||
//! `handlers/import.rs::get_preview_page` and `::api_get_preview` — which
|
||||
//! render/respond to `NotYetMapped` differently (redirect vs. status code); that
|
||||
//! decision stays in the handlers, not here.
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::AnnotatedRow,
|
||||
value_objects::{ImportSessionId, UserId},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::import::deps::GetPreviewStageDeps;
|
||||
|
||||
/// The columns and mapped/annotated rows for a session whose mapping has
|
||||
/// already been applied. `columns` comes from the session's `parsed_file` —
|
||||
/// the HTML preview template renders it as the table header — while `rows`
|
||||
/// comes from `row_results`. Not in the brief's `PreviewStage::Ready(Vec<AnnotatedRow>)`
|
||||
/// sketch: the deleted `get_preview_page` handler code read both
|
||||
/// `session.parsed_file.columns` and `session.row_results` to render the page,
|
||||
/// so dropping `columns` here would either blank the preview table's header or
|
||||
/// force the handler to re-fetch the session itself (forbidden — that's the
|
||||
/// exact repo call this task removes). See task-2 report for detail.
|
||||
pub struct PreviewRows {
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<AnnotatedRow>,
|
||||
}
|
||||
|
||||
pub enum PreviewStage {
|
||||
Ready(PreviewRows),
|
||||
NotYetMapped,
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
deps: &GetPreviewStageDeps,
|
||||
session_id: ImportSessionId,
|
||||
user_id: Uuid,
|
||||
) -> Result<PreviewStage, DomainError> {
|
||||
let user_id = UserId::from_uuid(user_id);
|
||||
let session = deps
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("session not found".into()))?;
|
||||
|
||||
let Some(rows) = session.row_results else {
|
||||
return Ok(PreviewStage::NotYetMapped);
|
||||
};
|
||||
|
||||
let columns = session.parsed_file.map(|p| p.columns).unwrap_or_default();
|
||||
|
||||
Ok(PreviewStage::Ready(PreviewRows { columns, rows }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_preview_stage.rs"]
|
||||
mod tests;
|
||||
43
crates/application/src/import/get_session_state.rs
Normal file
43
crates/application/src/import/get_session_state.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Backs `handlers/import.rs::api_get_session` — a plain state query, not a
|
||||
//! redirect-driving gate (the API has nothing to redirect to; a missing
|
||||
//! session is just a 404).
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
value_objects::{ImportSessionId, UserId},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::import::deps::GetSessionStateDeps;
|
||||
|
||||
pub struct SessionState {
|
||||
pub columns: Vec<String>,
|
||||
pub has_mappings: bool,
|
||||
pub row_count: usize,
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
deps: &GetSessionStateDeps,
|
||||
session_id: ImportSessionId,
|
||||
user_id: Uuid,
|
||||
) -> Result<SessionState, DomainError> {
|
||||
let user_id = UserId::from_uuid(user_id);
|
||||
let session = deps
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("session not found".into()))?;
|
||||
|
||||
let parsed = session.parsed_file.unwrap_or_default();
|
||||
let row_count = parsed.rows.len();
|
||||
|
||||
Ok(SessionState {
|
||||
columns: parsed.columns,
|
||||
has_mappings: session.field_mappings.is_some(),
|
||||
row_count,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_session_state.rs"]
|
||||
mod tests;
|
||||
@@ -1,15 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError, models::ImportProfile, ports::ImportProfileRepository,
|
||||
value_objects::UserId,
|
||||
};
|
||||
use crate::import::deps::ListImportProfilesDeps;
|
||||
use domain::{errors::DomainError, models::ImportProfile, value_objects::UserId};
|
||||
|
||||
pub async fn execute(
|
||||
import_profile: Arc<dyn ImportProfileRepository>,
|
||||
deps: &ListImportProfilesDeps,
|
||||
user_id: &UserId,
|
||||
) -> Result<Vec<ImportProfile>, DomainError> {
|
||||
import_profile.list_for_user(user_id).await
|
||||
deps.import_profile.list_for_user(user_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
pub mod apply_mapping;
|
||||
pub mod apply_profile;
|
||||
pub mod apply_profile_and_map;
|
||||
pub mod cleanup;
|
||||
pub mod commands;
|
||||
pub mod create_session;
|
||||
pub mod delete_profile;
|
||||
pub mod deps;
|
||||
pub mod execute;
|
||||
pub mod get_mapping_stage;
|
||||
pub mod get_preview_stage;
|
||||
pub mod get_session_state;
|
||||
pub mod list_profiles;
|
||||
pub mod save_profile;
|
||||
|
||||
108
crates/application/src/import/tests/apply_profile_and_map.rs
Normal file
108
crates/application/src/import/tests/apply_profile_and_map.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use domain::models::import::{DomainField, Transform};
|
||||
use domain::models::{FieldMapping, FileFormat, ImportProfile};
|
||||
use domain::ports::{ImportProfileRepository, ImportSessionRepository};
|
||||
use domain::testing::{InMemoryImportProfileRepository, InMemoryImportSessionRepository};
|
||||
use domain::value_objects::{ImportProfileId, UserId};
|
||||
|
||||
use crate::import::deps::{ApplyProfileAndMapDeps, CreateSessionDeps};
|
||||
use crate::import::{
|
||||
apply_profile_and_map, commands::ApplyProfileAndMapCommand,
|
||||
commands::CreateImportSessionCommand, create_session,
|
||||
};
|
||||
use crate::test_helpers::TestContextBuilder;
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_when_profile_not_found() {
|
||||
let profiles = InMemoryImportProfileRepository::new();
|
||||
let sessions = InMemoryImportSessionRepository::new();
|
||||
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 = InMemoryImportProfileRepository::new();
|
||||
let sessions = InMemoryImportSessionRepository::new();
|
||||
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,
|
||||
CreateImportSessionCommand {
|
||||
user_id,
|
||||
bytes: b"title\nTest".to_vec(),
|
||||
format: FileFormat::Csv,
|
||||
},
|
||||
)
|
||||
.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 = sessions
|
||||
.get(&created.session_id, &UserId::from_uuid(user_id))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(updated.row_results.is_some());
|
||||
assert!(updated.field_mappings.is_some());
|
||||
}
|
||||
@@ -3,14 +3,19 @@ use std::sync::Arc;
|
||||
use domain::testing::InMemoryImportProfileRepository;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::import::{commands::DeleteImportProfileCommand, delete_profile};
|
||||
use crate::import::{
|
||||
commands::DeleteImportProfileCommand, delete_profile, deps::DeleteImportProfileDeps,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_when_profile_not_found() {
|
||||
let profiles = InMemoryImportProfileRepository::new();
|
||||
let deps = DeleteImportProfileDeps {
|
||||
import_profile: Arc::clone(&profiles) as _,
|
||||
};
|
||||
|
||||
let result = delete_profile::execute(
|
||||
Arc::clone(&profiles) as _,
|
||||
&deps,
|
||||
DeleteImportProfileCommand {
|
||||
user_id: Uuid::new_v4(),
|
||||
profile_id: Uuid::new_v4(),
|
||||
|
||||
68
crates/application/src/import/tests/get_mapping_stage.rs
Normal file
68
crates/application/src/import/tests/get_mapping_stage.rs
Normal 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::InMemoryImportSessionRepository;
|
||||
use domain::value_objects::{ImportSessionId, UserId};
|
||||
|
||||
use crate::import::deps::GetMappingStageDeps;
|
||||
use crate::import::get_mapping_stage::{self, SAMPLE_ROW_LIMIT};
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_mapping_stage_is_not_found_when_file_not_parsed() {
|
||||
let sessions = InMemoryImportSessionRepository::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 = InMemoryImportSessionRepository::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 = InMemoryImportSessionRepository::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);
|
||||
}
|
||||
80
crates/application/src/import/tests/get_preview_stage.rs
Normal file
80
crates/application/src/import/tests/get_preview_stage.rs
Normal 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::InMemoryImportSessionRepository;
|
||||
use domain::value_objects::{ImportSessionId, UserId};
|
||||
|
||||
use crate::import::deps::GetPreviewStageDeps;
|
||||
use crate::import::get_preview_stage::{self, PreviewStage};
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_preview_stage_reports_not_yet_mapped_when_row_results_absent() {
|
||||
let sessions = InMemoryImportSessionRepository::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 = InMemoryImportSessionRepository::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 = InMemoryImportSessionRepository::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());
|
||||
}
|
||||
50
crates/application/src/import/tests/get_session_state.rs
Normal file
50
crates/application/src/import/tests/get_session_state.rs
Normal 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::InMemoryImportSessionRepository;
|
||||
use domain::value_objects::{ImportSessionId, UserId};
|
||||
|
||||
use crate::import::deps::GetSessionStateDeps;
|
||||
use crate::import::get_session_state;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_session_state_is_not_found_when_session_missing() {
|
||||
let sessions = InMemoryImportSessionRepository::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 = InMemoryImportSessionRepository::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);
|
||||
}
|
||||
@@ -4,16 +4,17 @@ use domain::testing::InMemoryImportProfileRepository;
|
||||
use domain::value_objects::UserId;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::import::list_profiles;
|
||||
use crate::import::{deps::ListImportProfilesDeps, list_profiles};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_when_no_profiles() {
|
||||
let profiles = InMemoryImportProfileRepository::new();
|
||||
let deps = ListImportProfilesDeps {
|
||||
import_profile: Arc::clone(&profiles) as _,
|
||||
};
|
||||
|
||||
let user_id = UserId::from_uuid(Uuid::new_v4());
|
||||
let result = list_profiles::execute(Arc::clone(&profiles) as _, &user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let result = list_profiles::execute(&deps, &user_id).await.unwrap();
|
||||
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user