test: cascading in-memory store
This commit is contained in:
200
crates/domain/src/testing/fakes/store_movie.rs
Normal file
200
crates/domain/src/testing/fakes/store_movie.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
errors::DomainError,
|
||||
models::{
|
||||
Movie, MovieFilter, MovieProfile, MovieSummary, PersistedWatchEvent, WatchEvent,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
ports::{MovieCommand, MovieProfileRepository, MovieQuery},
|
||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
||||
};
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait]
|
||||
impl MovieCommand for InMemoryStore {
|
||||
/// `movies.external_metadata_id` is UNIQUE: another movie row holding the
|
||||
/// same external id is a constraint violation, which the sqlite adapter
|
||||
/// surfaces as an infrastructure error.
|
||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||
let mut movies = self.movies.write().unwrap();
|
||||
if let Some(external) = movie.external_metadata_id() {
|
||||
let taken = movies.values().any(|m| {
|
||||
m.id() != movie.id()
|
||||
&& m.external_metadata_id().map(|e| e.value()) == Some(external.value())
|
||||
});
|
||||
if taken {
|
||||
return Err(DomainError::InfrastructureError(
|
||||
"Database operation failed".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
movies.insert(movie.id().value(), movie.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schema: `reviews.movie_id REFERENCES movies(id)` has no cascade, so a
|
||||
/// movie with reviews cannot be deleted. `watchlist_entries` and the
|
||||
/// `movie_*` profile tables cascade; `watch_events.movie_id` is set to NULL.
|
||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||
let id = movie_id.value();
|
||||
if !self.movies.read().unwrap().contains_key(&id) {
|
||||
return Ok(());
|
||||
}
|
||||
if self
|
||||
.reviews
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.any(|r| r.movie_id().value() == id)
|
||||
{
|
||||
return Err(DomainError::InfrastructureError(
|
||||
"Database operation failed".into(),
|
||||
));
|
||||
}
|
||||
self.movies.write().unwrap().remove(&id);
|
||||
self.watchlist
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|(_, movie), _| *movie != id);
|
||||
self.movie_profiles.write().unwrap().remove(&id);
|
||||
let mut events = self.watch_events.write().unwrap();
|
||||
for event in events.iter_mut() {
|
||||
if event.movie_id().map(|m| m.value()) == Some(id) {
|
||||
*event = detached_from_movie(event);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieQuery for InMemoryStore {
|
||||
async fn get_movie_by_external_id(
|
||||
&self,
|
||||
external_metadata_id: &ExternalMetadataId,
|
||||
) -> Result<Option<Movie>, DomainError> {
|
||||
let store = self.movies.read().unwrap();
|
||||
Ok(store
|
||||
.values()
|
||||
.find(|m| {
|
||||
m.external_metadata_id()
|
||||
.map(|e| e.value() == external_metadata_id.value())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn get_movie_by_id(&self, movie_id: &MovieId) -> Result<Option<Movie>, DomainError> {
|
||||
Ok(self.movies.read().unwrap().get(&movie_id.value()).cloned())
|
||||
}
|
||||
|
||||
async fn get_movies_by_title_and_year(
|
||||
&self,
|
||||
title: &MovieTitle,
|
||||
year: &ReleaseYear,
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
let store = self.movies.read().unwrap();
|
||||
Ok(store
|
||||
.values()
|
||||
.filter(|m| m.title() == title && m.release_year() == year)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn existing_external_ids(
|
||||
&self,
|
||||
ids: &[ExternalMetadataId],
|
||||
) -> Result<std::collections::HashSet<String>, DomainError> {
|
||||
let store = self.movies.read().unwrap();
|
||||
let known: std::collections::HashSet<String> = store
|
||||
.values()
|
||||
.filter_map(|m| m.external_metadata_id().map(|e| e.value().to_string()))
|
||||
.collect();
|
||||
Ok(ids
|
||||
.iter()
|
||||
.map(|id| id.value().to_string())
|
||||
.filter(|v| known.contains(v))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn existing_title_year_pairs(
|
||||
&self,
|
||||
pairs: &[(MovieTitle, ReleaseYear)],
|
||||
) -> Result<std::collections::HashSet<(String, u16)>, DomainError> {
|
||||
let store = self.movies.read().unwrap();
|
||||
let known: std::collections::HashSet<(String, u16)> = store
|
||||
.values()
|
||||
.map(|m| (m.title().value().to_string(), m.release_year().value()))
|
||||
.collect();
|
||||
Ok(pairs
|
||||
.iter()
|
||||
.map(|(t, y)| (t.value().to_string(), y.value()))
|
||||
.filter(|p| known.contains(p))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_page: &PageParams,
|
||||
_filter: &MovieFilter,
|
||||
) -> Result<Paginated<MovieSummary>, DomainError> {
|
||||
Ok(Paginated {
|
||||
items: vec![],
|
||||
total_count: 0,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_movies_with_external_id(&self) -> Result<Vec<Movie>, DomainError> {
|
||||
Ok(self
|
||||
.movies
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|m| m.external_metadata_id().is_some())
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieProfileRepository for InMemoryStore {
|
||||
async fn upsert(&self, profile: &MovieProfile) -> Result<(), DomainError> {
|
||||
self.movie_profiles
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(profile.movie_id.value(), profile.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_by_movie_id(&self, id: &MovieId) -> Result<Option<MovieProfile>, DomainError> {
|
||||
Ok(self
|
||||
.movie_profiles
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&id.value())
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn list_stale(&self) -> Result<Vec<(MovieId, String)>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn detached_from_movie(event: &WatchEvent) -> WatchEvent {
|
||||
WatchEvent::from_persistence(PersistedWatchEvent {
|
||||
id: event.id().clone(),
|
||||
user_id: event.user_id().clone(),
|
||||
movie_id: None,
|
||||
title: event.title().to_string(),
|
||||
year: event.year(),
|
||||
external_metadata_id: event.external_metadata_id().map(str::to_string),
|
||||
source: event.source().clone(),
|
||||
watched_at: *event.watched_at(),
|
||||
status: event.status().clone(),
|
||||
created_at: *event.created_at(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user