code refactor
Some checks failed
CI / Check / Test (push) Has been cancelled

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-10-26 09:36:23 +00:00
parent 2ce7db12c2
commit 017b58d6fa
804 changed files with 33423 additions and 28955 deletions

View File

@@ -0,0 +1,162 @@
use crate::support::{add_to_watchlist_command, movie_input_manual};
use domain::testing::test_movie_with_year;
use std::sync::Arc;
use domain::{
events::DomainEvent,
ports::MovieCommand,
testing::{InMemoryStore, RefusingWatchlistRepository},
};
use application::watchlist::{add, commands::AddToWatchlistCommand, deps::WatchlistAddDeps};
fn make_deps(movies: Arc<InMemoryStore>, watchlist: Arc<InMemoryStore>) -> WatchlistAddDeps {
WatchlistAddDeps {
movie_command: Arc::clone(&movies) as _,
movie_query: movies,
metadata: Arc::new(domain::testing::FakeMetadataClient),
watchlist,
}
}
#[tokio::test]
async fn adding_to_the_watchlist_resolves_the_movie_and_saves_the_entry() {
let movies = InMemoryStore::new();
let watchlist = Arc::clone(&movies);
let movie = test_movie_with_year("The Thing", 1982);
let movie_uuid = movie.id().value();
movies.upsert_movie(&movie, &[]).await.unwrap();
let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist));
let cmd = add_to_watchlist_command(uuid::Uuid::new_v4(), movie_uuid);
add::execute(&deps, cmd).await.unwrap();
assert_eq!(watchlist.watchlist_count(), 1);
}
#[tokio::test]
async fn adding_a_movie_already_on_the_watchlist_is_idempotent() {
let movies = InMemoryStore::new();
let watchlist = Arc::clone(&movies);
let movie = test_movie_with_year("RoboCop", 1987);
let movie_uuid = movie.id().value();
let user_id = uuid::Uuid::new_v4();
movies.upsert_movie(&movie, &[]).await.unwrap();
let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist));
let make_cmd = || add_to_watchlist_command(user_id, movie_uuid);
add::execute(&deps, make_cmd()).await.unwrap();
add::execute(&deps, make_cmd()).await.unwrap();
assert_eq!(
movies.recorded_events().len(),
1,
"the repeat inserted nothing, so it announces nothing"
);
assert_eq!(
watchlist.watchlist_count(),
1,
"idempotent add should not duplicate"
);
}
#[tokio::test]
async fn adding_a_manual_movie_to_the_watchlist_creates_and_saves_it() {
let movies = InMemoryStore::new();
let watchlist = Arc::clone(&movies);
let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist));
let cmd = AddToWatchlistCommand {
user_id: uuid::Uuid::new_v4(),
input: movie_input_manual("New Manual Movie", 2024),
};
add::execute(&deps, cmd).await.unwrap();
assert_eq!(watchlist.watchlist_count(), 1);
assert_eq!(movies.movie_count(), 1);
}
#[tokio::test]
async fn adding_an_unknown_movie_id_to_the_watchlist_fails() {
let movies = InMemoryStore::new();
let watchlist = Arc::clone(&movies);
let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist));
let cmd = add_to_watchlist_command(uuid::Uuid::new_v4(), uuid::Uuid::new_v4());
assert!(add::execute(&deps, cmd).await.is_err());
}
#[tokio::test]
async fn adding_the_same_movie_to_a_watchlist_twice_keeps_a_single_entry_and_succeeds() {
let movies = InMemoryStore::new();
let movie = test_movie_with_year("Heat", 1995);
let movie_uuid = movie.id().value();
let user_id = uuid::Uuid::new_v4();
movies.upsert_movie(&movie, &[]).await.unwrap();
let deps = make_deps(Arc::clone(&movies), Arc::clone(&movies));
for _ in 0..2 {
add::execute(&deps, add_to_watchlist_command(user_id, movie_uuid))
.await
.expect("second add must not error");
}
assert_eq!(movies.watchlist_count(), 1);
}
#[tokio::test]
async fn adding_an_entry_records_the_entry_and_its_event_together() {
let movies = InMemoryStore::new();
let movie = test_movie_with_year("Heat", 1995);
movies.upsert_movie(&movie, &[]).await.unwrap();
let deps = make_deps(Arc::clone(&movies), Arc::clone(&movies));
add::execute(
&deps,
add_to_watchlist_command(uuid::Uuid::new_v4(), movie.id().value()),
)
.await
.unwrap();
assert_eq!(movies.watchlist_count(), 1);
let recorded = movies.recorded_events();
assert_eq!(recorded.len(), 1);
assert!(matches!(
recorded[0],
DomainEvent::WatchlistEntryAdded { .. }
));
}
#[tokio::test]
async fn a_refused_add_propagates_and_records_nothing() {
let movies = InMemoryStore::new();
let movie = test_movie_with_year("Heat", 1995);
movies.upsert_movie(&movie, &[]).await.unwrap();
let deps = WatchlistAddDeps {
watchlist: Arc::new(RefusingWatchlistRepository),
..make_deps(Arc::clone(&movies), Arc::clone(&movies))
};
let result = add::execute(
&deps,
add_to_watchlist_command(uuid::Uuid::new_v4(), movie.id().value()),
)
.await;
assert!(matches!(
result,
Err(domain::errors::DomainError::InfrastructureError(m)) if m == "refused"
));
assert_eq!(movies.watchlist_count(), 0);
assert!(movies.recorded_events().is_empty());
}

View File

@@ -0,0 +1,25 @@
use uuid::Uuid;
use crate::support::TestContextBuilder;
use application::watchlist::{deps::GetWatchlistDeps, get, queries::GetWatchlistQuery};
#[tokio::test]
async fn returns_empty_page_for_new_user() {
let b = TestContextBuilder::new();
let deps = GetWatchlistDeps {
watchlist: b.watchlist_repo.clone(),
};
let result = get::execute(
&deps,
GetWatchlistQuery {
user_id: Uuid::new_v4(),
limit: None,
offset: None,
},
)
.await
.unwrap();
assert!(result.items.is_empty());
assert_eq!(result.total_count, 0);
}

View File

@@ -0,0 +1,121 @@
use crate::support::register_command;
use std::sync::Arc;
use uuid::Uuid;
use domain::testing::{InMemoryStore, PanicRemoteWatchlistRepository, PanicWatchlistRepository};
use domain::value_objects::Email;
use crate::support::TestContextBuilder;
use application::auth::deps::RegisterDeps;
use application::auth::register;
use application::watchlist::deps::GetWatchlistForOwnerDeps;
use application::watchlist::get_watchlist_for_owner::{self, WatchlistView};
async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) {
let deps = RegisterDeps {
user: b.user_repo.clone(),
password_hasher: b.password_hasher.clone(),
config: b.config.clone(),
};
register::execute(&deps, register_command(email, username))
.await
.unwrap();
}
#[tokio::test]
async fn watchlist_for_owner_returns_local_when_user_row_exists() {
let b = TestContextBuilder::new();
setup_user(&b, "owner@test.com", "owneruser").await;
let email = Email::new("owner@test.com".into()).unwrap();
let user = b.user_repo.find_by_email(&email).await.unwrap().unwrap();
let uid = user.id().value();
let deps = GetWatchlistForOwnerDeps {
user: b.user_repo.clone(),
watchlist: InMemoryStore::new(),
remote_watchlist: Arc::new(PanicRemoteWatchlistRepository),
};
let view = get_watchlist_for_owner::execute(&deps, uid, Some(20), Some(0))
.await
.unwrap();
assert!(matches!(view, WatchlistView::Local(_)), "expected Local");
}
#[tokio::test]
async fn watchlist_for_owner_returns_remote_when_no_local_row() {
let owner_id = Uuid::new_v4();
let remote = InMemoryStore::with_remote_watchlist(vec![domain::models::RemoteWatchlistEntry {
ap_id: "https://remote.example/ap/1".into(),
actor_url: "https://remote.example/users/owner".into(),
movie_title: "Remote Movie".into(),
release_year: 2021,
external_metadata_id: None,
poster_url: None,
added_at: chrono::Utc::now(),
}]);
let deps = GetWatchlistForOwnerDeps {
user: InMemoryStore::new(),
watchlist: Arc::new(PanicWatchlistRepository),
remote_watchlist: remote,
};
let view = get_watchlist_for_owner::execute(&deps, owner_id, Some(20), Some(0))
.await
.unwrap();
match view {
WatchlistView::Remote(entries) => {
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].movie_title, "Remote Movie");
}
WatchlistView::Local(_) => panic!("expected Remote"),
}
}
#[tokio::test]
async fn watchlist_for_owner_local_arm_validates_limit() {
let b = TestContextBuilder::new();
setup_user(&b, "badlimit@test.com", "badlimituser").await;
let email = Email::new("badlimit@test.com".into()).unwrap();
let user = b.user_repo.find_by_email(&email).await.unwrap().unwrap();
let uid = user.id().value();
let deps = GetWatchlistForOwnerDeps {
user: b.user_repo.clone(),
watchlist: InMemoryStore::new(),
remote_watchlist: Arc::new(PanicRemoteWatchlistRepository),
};
let err = match get_watchlist_for_owner::execute(&deps, uid, Some(0), Some(0)).await {
Err(e) => e,
Ok(_) => panic!("expected Err(ValidationError) for limit=0 on the local arm, got Ok"),
};
assert!(matches!(
err,
domain::errors::DomainError::ValidationError(_)
));
}
#[tokio::test]
async fn watchlist_for_owner_remote_arm_ignores_invalid_limit() {
let owner_id = Uuid::new_v4();
let remote = InMemoryStore::new();
let deps = GetWatchlistForOwnerDeps {
user: InMemoryStore::new(),
watchlist: Arc::new(PanicWatchlistRepository),
remote_watchlist: remote,
};
let view = get_watchlist_for_owner::execute(&deps, owner_id, Some(0), Some(0))
.await
.unwrap();
assert!(matches!(view, WatchlistView::Remote(_)), "expected Remote");
}

View File

@@ -0,0 +1,58 @@
use std::sync::Arc;
use domain::models::WatchlistEntry;
use domain::ports::WatchlistRepository;
use domain::testing::InMemoryStore;
use domain::value_objects::{MovieId, UserId};
use uuid::Uuid;
use crate::support::TestContextBuilder;
use application::watchlist::{deps::IsOnWatchlistDeps, is_on, queries::IsOnWatchlistQuery};
#[tokio::test]
async fn returns_true_when_present() {
let watchlist = InMemoryStore::new();
let uid = Uuid::new_v4();
let mid = Uuid::new_v4();
watchlist
.add(
&WatchlistEntry::new(UserId::from_uuid(uid), MovieId::from_uuid(mid)),
&[],
)
.await
.unwrap();
let deps = IsOnWatchlistDeps {
watchlist: Arc::clone(&watchlist) as _,
};
let result = is_on::execute(
&deps,
IsOnWatchlistQuery {
user_id: uid,
movie_id: mid,
},
)
.await
.unwrap();
assert!(result);
}
#[tokio::test]
async fn returns_false_when_absent() {
let b = TestContextBuilder::new();
let deps = IsOnWatchlistDeps {
watchlist: b.watchlist_repo.clone(),
};
let result = is_on::execute(
&deps,
IsOnWatchlistQuery {
user_id: Uuid::new_v4(),
movie_id: Uuid::new_v4(),
},
)
.await
.unwrap();
assert!(!result);
}

View File

@@ -0,0 +1,107 @@
use std::sync::Arc;
use domain::events::DomainEvent;
use domain::models::WatchlistEntry;
use domain::ports::WatchlistRepository;
use domain::testing::{InMemoryStore, RefusingWatchlistRepository};
use domain::value_objects::{MovieId, UserId};
use uuid::Uuid;
use crate::support::TestContextBuilder;
use application::watchlist::{
commands::RemoveFromWatchlistCommand, deps::RemoveFromWatchlistDeps, remove,
};
#[tokio::test]
async fn removing_an_entry_records_the_removal_and_its_event_together() {
let watchlist = InMemoryStore::new();
let uid = Uuid::new_v4();
let mid = Uuid::new_v4();
watchlist
.add(
&WatchlistEntry::new(UserId::from_uuid(uid), MovieId::from_uuid(mid)),
&[],
)
.await
.unwrap();
let deps = RemoveFromWatchlistDeps {
watchlist: Arc::clone(&watchlist) as _,
};
remove::execute(
&deps,
RemoveFromWatchlistCommand {
user_id: uid,
movie_id: mid,
},
)
.await
.unwrap();
assert_eq!(watchlist.watchlist_count(), 0);
let recorded = watchlist.recorded_events();
assert_eq!(recorded.len(), 1);
assert!(matches!(
recorded[0],
DomainEvent::WatchlistEntryRemoved { .. }
));
}
#[tokio::test]
async fn fails_when_not_on_watchlist() {
let b = TestContextBuilder::new();
let deps = RemoveFromWatchlistDeps {
watchlist: b.watchlist_repo.clone(),
};
let result = remove::execute(
&deps,
RemoveFromWatchlistCommand {
user_id: Uuid::new_v4(),
movie_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn a_failed_removal_records_no_event() {
let store = InMemoryStore::new();
let deps = RemoveFromWatchlistDeps {
watchlist: Arc::clone(&store) as _,
};
let result = remove::execute(
&deps,
RemoveFromWatchlistCommand {
user_id: Uuid::new_v4(),
movie_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
assert!(store.recorded_events().is_empty());
}
#[tokio::test]
async fn a_refused_remove_propagates_the_refusal() {
let deps = RemoveFromWatchlistDeps {
watchlist: Arc::new(RefusingWatchlistRepository),
};
let result = remove::execute(
&deps,
RemoveFromWatchlistCommand {
user_id: Uuid::new_v4(),
movie_id: Uuid::new_v4(),
},
)
.await;
assert!(matches!(
result,
Err(domain::errors::DomainError::InfrastructureError(m)) if m == "refused"
));
}