refactor/tighten-architecture #26
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1663,6 +1663,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"domain",
|
||||
"email_address",
|
||||
"futures",
|
||||
"serde",
|
||||
|
||||
@@ -16,6 +16,7 @@ serde_json = { workspace = true }
|
||||
email_address = "0.2.9"
|
||||
|
||||
[dev-dependencies]
|
||||
domain = { path = ".", features = ["test-helpers"] }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -26,7 +26,3 @@ impl DomainError {
|
||||
matches!(self, DomainError::InfrastructureError(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -168,7 +168,3 @@ impl EventEnvelope {
|
||||
self.ack.nack().await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/events.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -109,7 +109,3 @@ impl GoalWithProgress {
|
||||
self.current_count >= self.goal.target_count
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/goal.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -103,7 +103,3 @@ pub enum ExportFormat {
|
||||
Csv,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/mod.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -181,7 +181,3 @@ pub struct CrewCredit {
|
||||
pub department: String,
|
||||
pub poster_path: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/person.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -202,7 +202,3 @@ impl ReviewHistory {
|
||||
self.viewings.sort_by_key(|r| *r.watched_at());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/review.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -138,7 +138,3 @@ pub struct MovieStats {
|
||||
pub federated_count: u64,
|
||||
pub rating_histogram: [u64; 5], // index 0 = 1★, index 4 = 5★
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/stats.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -234,7 +234,3 @@ pub struct ParsedPlaybackEvent {
|
||||
pub tmdb_id: Option<String>,
|
||||
pub imdb_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/watch_event.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -49,7 +49,3 @@ pub trait ApBackfillPort: Send + Sync {
|
||||
follower_inbox_url: String,
|
||||
) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/federation.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -150,164 +150,3 @@ impl<T: SocialCommand + FollowGraphQuery + BlockQuery + FollowTargetResolver + R
|
||||
LocalSocial for T
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod local_social_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Stub;
|
||||
|
||||
#[async_trait]
|
||||
impl SocialCommand for Stub {
|
||||
async fn follow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::FollowTarget,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unfollow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn accept_follow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn reject_follow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_follower(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn block(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unblock(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FollowGraphQuery for Stub {
|
||||
async fn get_following(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_following(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn count_following(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_pending_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn get_relation(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<crate::value_objects::FollowRelation, DomainError> {
|
||||
Err(DomainError::NotFound("stub".into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BlockQuery for Stub {
|
||||
async fn get_blocked(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FollowTargetResolver for Stub {
|
||||
async fn resolve_target(
|
||||
&self,
|
||||
target: &crate::value_objects::FollowTarget,
|
||||
) -> Result<SocialIdentity, DomainError> {
|
||||
match target {
|
||||
crate::value_objects::FollowTarget::Identity(id) => Ok(id.clone()),
|
||||
crate::value_objects::FollowTarget::Handle(handle) => Ok(SocialIdentity::Remote {
|
||||
actor_url: handle.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolvedFollow for Stub {
|
||||
async fn follow_resolved(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blanket_impl_yields_a_trait_object() {
|
||||
let local: Arc<dyn LocalSocial> = Arc::new(Stub);
|
||||
// Reachable through each supertrait without a separate Arc.
|
||||
let _: &dyn SocialCommand = local.as_ref();
|
||||
let _: &dyn FollowGraphQuery = local.as_ref();
|
||||
let _: &dyn BlockQuery = local.as_ref();
|
||||
let _: &dyn FollowTargetResolver = local.as_ref();
|
||||
let _: &dyn ResolvedFollow = local.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,3 @@ fn label_of(insight: &Insight) -> &str {
|
||||
fn round2(v: f64) -> f64 {
|
||||
(v * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/insights_analyzer.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -51,7 +51,3 @@ impl ReviewHistoryAnalyzer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/review_history.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -464,7 +464,3 @@ fn compute_global_stats(
|
||||
|
||||
(most_active_user, most_watched, total_users_active)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/wrapup_analyzer.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -13,8 +13,6 @@ mod store_user;
|
||||
mod store_watch_event;
|
||||
mod store_watchlist;
|
||||
mod stubs;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod wrapup;
|
||||
|
||||
pub use insights::*;
|
||||
|
||||
@@ -11,7 +11,3 @@ pub use movie::*;
|
||||
pub use review::*;
|
||||
pub use social::*;
|
||||
pub use user::*;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../tests/value_objects.rs"]
|
||||
mod tests;
|
||||
|
||||
2
crates/domain/tests/errors.rs
Normal file
2
crates/domain/tests/errors.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "errors/errors_test.rs"]
|
||||
mod errors_test;
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use domain::errors::DomainError;
|
||||
|
||||
#[test]
|
||||
fn infrastructure_error_is_transient() {
|
||||
2
crates/domain/tests/events.rs
Normal file
2
crates/domain/tests/events.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "events/events_test.rs"]
|
||||
mod events_test;
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::value_objects::{FollowTarget, SocialIdentity, UserId};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::value_objects::{FollowTarget, SocialIdentity, UserId};
|
||||
|
||||
#[test]
|
||||
fn follow_accepted_matches() {
|
||||
12
crates/domain/tests/models.rs
Normal file
12
crates/domain/tests/models.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
#[path = "models/constructors_test.rs"]
|
||||
mod constructors_test;
|
||||
#[path = "models/goal_test.rs"]
|
||||
mod goal_test;
|
||||
#[path = "models/person_test.rs"]
|
||||
mod person_test;
|
||||
#[path = "models/review_test.rs"]
|
||||
mod review_test;
|
||||
#[path = "models/stats_test.rs"]
|
||||
mod stats_test;
|
||||
#[path = "models/watch_event_test.rs"]
|
||||
mod watch_event_test;
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::testing::{test_movie_with_director, test_movie_with_year, test_review, test_user};
|
||||
use crate::value_objects::{Email, PasswordHash, UserId, Username};
|
||||
use domain::models::*;
|
||||
use domain::testing::{test_movie_with_director, test_movie_with_year, test_review, test_user};
|
||||
use domain::value_objects::{Email, PasswordHash, UserId, Username};
|
||||
|
||||
#[test]
|
||||
fn update_profile_sets_fields() {
|
||||
@@ -32,11 +32,11 @@ fn update_profile_clears_with_none() {
|
||||
#[test]
|
||||
fn movie_new_sets_fields() {
|
||||
let m = Movie::new(
|
||||
Some(crate::value_objects::ExternalMetadataId::new("tt1234567".into()).unwrap()),
|
||||
crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(),
|
||||
crate::value_objects::ReleaseYear::new(1982).unwrap(),
|
||||
Some(domain::value_objects::ExternalMetadataId::new("tt1234567".into()).unwrap()),
|
||||
domain::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(),
|
||||
domain::value_objects::ReleaseYear::new(1982).unwrap(),
|
||||
Some("Ridley Scott".into()),
|
||||
Some(crate::value_objects::PosterPath::new("/poster.jpg".into()).unwrap()),
|
||||
Some(domain::value_objects::PosterPath::new("/poster.jpg".into()).unwrap()),
|
||||
);
|
||||
assert_eq!(m.title().value(), "Blade Runner");
|
||||
assert_eq!(m.release_year().value(), 1982);
|
||||
@@ -48,14 +48,14 @@ fn movie_new_sets_fields() {
|
||||
#[test]
|
||||
fn movie_update_poster() {
|
||||
let mut m = test_movie_with_year("Blade Runner", 1982);
|
||||
let new_poster = crate::value_objects::PosterPath::new("/new.jpg".into()).unwrap();
|
||||
let new_poster = domain::value_objects::PosterPath::new("/new.jpg".into()).unwrap();
|
||||
m.update_poster(new_poster);
|
||||
assert_eq!(m.poster_path().unwrap().value(), "/new.jpg");
|
||||
}
|
||||
|
||||
// ── Review ───────────────────────────────────────────────────────────────────
|
||||
|
||||
use crate::value_objects::{MovieId, Rating, ReviewId};
|
||||
use domain::value_objects::{MovieId, Rating, ReviewId};
|
||||
|
||||
#[test]
|
||||
fn review_new_sets_local_source() {
|
||||
@@ -199,16 +199,16 @@ fn month_activity_construction() {
|
||||
#[test]
|
||||
fn movie_is_manual_match_same_title_year() {
|
||||
let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott");
|
||||
let title = crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
|
||||
let year = crate::value_objects::ReleaseYear::new(1982).unwrap();
|
||||
let title = domain::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
|
||||
let year = domain::value_objects::ReleaseYear::new(1982).unwrap();
|
||||
assert!(m.is_manual_match(&title, &year, Some("ridley scott")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movie_is_manual_match_different_director_fails() {
|
||||
let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott");
|
||||
let title = crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
|
||||
let year = crate::value_objects::ReleaseYear::new(1982).unwrap();
|
||||
let title = domain::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
|
||||
let year = domain::value_objects::ReleaseYear::new(1982).unwrap();
|
||||
assert!(!m.is_manual_match(&title, &year, Some("Denis Villeneuve")));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::testing::test_goal;
|
||||
use crate::value_objects::UserId;
|
||||
use domain::models::*;
|
||||
use domain::testing::test_goal;
|
||||
use domain::value_objects::{GoalId, UserId};
|
||||
|
||||
#[test]
|
||||
fn new_goal_valid() {
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use domain::models::*;
|
||||
|
||||
#[test]
|
||||
fn person_new() {
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::testing::{test_review, test_watched_at};
|
||||
use crate::value_objects::{Comment, MovieId, Rating, UserId, WatchMedium};
|
||||
use domain::testing::{test_review, test_watched_at};
|
||||
use domain::value_objects::{Comment, MovieId, Rating, UserId, WatchMedium};
|
||||
|
||||
use super::*;
|
||||
use domain::models::*;
|
||||
|
||||
fn review_with_medium(watch_medium: Option<WatchMedium>) -> Review {
|
||||
test_review(&MovieId::generate(), &UserId::generate(), 4).apply_edit(ReviewEdit {
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use domain::models::*;
|
||||
|
||||
fn appearance(person_id: u64, name: &str, billing: u32, movie: &str) -> ActorAppearance {
|
||||
ActorAppearance {
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use chrono::NaiveDateTime;
|
||||
use domain::models::*;
|
||||
use domain::value_objects::{MovieId, UserId, WebhookTokenId};
|
||||
|
||||
fn ts() -> NaiveDateTime {
|
||||
chrono::NaiveDate::from_ymd_opt(2024, 6, 1)
|
||||
4
crates/domain/tests/ports.rs
Normal file
4
crates/domain/tests/ports.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[path = "ports/federation_test.rs"]
|
||||
mod federation_test;
|
||||
#[path = "ports/social_test.rs"]
|
||||
mod social_test;
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist};
|
||||
use domain::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist};
|
||||
use domain::ports::{ApBackfillPort, ApDocumentPort, InstanceBlocklistPort};
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_document_returns_empty_strings() {
|
||||
162
crates/domain/tests/ports/social_test.rs
Normal file
162
crates/domain/tests/ports/social_test.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use async_trait::async_trait;
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::{
|
||||
BlockQuery, FollowGraphQuery, FollowTargetResolver, LocalSocial, ResolvedFollow, SocialCommand,
|
||||
};
|
||||
use domain::value_objects::SocialIdentity;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Stub;
|
||||
|
||||
#[async_trait]
|
||||
impl SocialCommand for Stub {
|
||||
async fn follow(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::FollowTarget,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unfollow(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn accept_follow(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn reject_follow(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_follower(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn block(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unblock(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FollowGraphQuery for Stub {
|
||||
async fn get_following(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_followers(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_followers(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_following(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn count_following(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_followers(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_pending_followers(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn get_relation(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::SocialIdentity,
|
||||
) -> Result<domain::value_objects::FollowRelation, DomainError> {
|
||||
Err(DomainError::NotFound("stub".into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BlockQuery for Stub {
|
||||
async fn get_blocked(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FollowTargetResolver for Stub {
|
||||
async fn resolve_target(
|
||||
&self,
|
||||
target: &domain::value_objects::FollowTarget,
|
||||
) -> Result<SocialIdentity, DomainError> {
|
||||
match target {
|
||||
domain::value_objects::FollowTarget::Identity(id) => Ok(id.clone()),
|
||||
domain::value_objects::FollowTarget::Handle(handle) => Ok(SocialIdentity::Remote {
|
||||
actor_url: handle.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolvedFollow for Stub {
|
||||
async fn follow_resolved(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blanket_impl_yields_a_trait_object() {
|
||||
let local: Arc<dyn LocalSocial> = Arc::new(Stub);
|
||||
// Reachable through each supertrait without a separate Arc.
|
||||
let _: &dyn SocialCommand = local.as_ref();
|
||||
let _: &dyn FollowGraphQuery = local.as_ref();
|
||||
let _: &dyn BlockQuery = local.as_ref();
|
||||
let _: &dyn FollowTargetResolver = local.as_ref();
|
||||
let _: &dyn ResolvedFollow = local.as_ref();
|
||||
}
|
||||
6
crates/domain/tests/services.rs
Normal file
6
crates/domain/tests/services.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#[path = "services/insights_analyzer_test.rs"]
|
||||
mod insights_analyzer_test;
|
||||
#[path = "services/review_history_test.rs"]
|
||||
mod review_history_test;
|
||||
#[path = "services/wrapup_analyzer_test.rs"]
|
||||
mod wrapup_analyzer_test;
|
||||
@@ -1,9 +1,9 @@
|
||||
use chrono::{Datelike, FixedOffset, NaiveDate, Utc, Weekday};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{Insight, WatchRow};
|
||||
use domain::models::{Insight, WatchRow};
|
||||
|
||||
use super::super::insights_analyzer::build_insights;
|
||||
use domain::services::insights_analyzer::build_insights;
|
||||
|
||||
fn watch(genres: &[&str], rating: u8, date: NaiveDate, hour: u32) -> WatchRow {
|
||||
WatchRow {
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::*;
|
||||
use crate::models::{Review, ReviewHistory};
|
||||
use crate::testing::test_movie_with_year;
|
||||
use crate::value_objects::{MovieId, Rating, UserId};
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use domain::models::{Review, ReviewHistory};
|
||||
use domain::services::review_history::{ReviewHistoryAnalyzer, Trend};
|
||||
use domain::testing::test_movie_with_year;
|
||||
use domain::value_objects::{MovieId, Rating, UserId};
|
||||
|
||||
fn dt(year: i32, month: u32, day: u32) -> NaiveDateTime {
|
||||
NaiveDateTime::new(
|
||||
@@ -1,10 +1,10 @@
|
||||
use chrono::NaiveDate;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::WrapUpMovieRow;
|
||||
use crate::models::wrapup::{DateRange, WrapUpScope};
|
||||
use domain::models::WrapUpMovieRow;
|
||||
use domain::models::wrapup::{DateRange, WrapUpScope};
|
||||
|
||||
use super::super::wrapup_analyzer::build_report;
|
||||
use domain::services::wrapup_analyzer::build_report;
|
||||
|
||||
fn row(title: &str, rating: u8, ym: &str) -> WrapUpMovieRow {
|
||||
WrapUpMovieRow {
|
||||
2
crates/domain/tests/testing.rs
Normal file
2
crates/domain/tests/testing.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "testing/in_memory_store_test.rs"]
|
||||
mod in_memory_store_test;
|
||||
@@ -3,21 +3,21 @@ use std::sync::Arc;
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{InMemoryStore, MOVIE_CASCADE_TABLES, USER_CASCADE_TABLES};
|
||||
use crate::errors::DomainError;
|
||||
use crate::models::{
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::{
|
||||
CastMember, CrewMember, Genre, Keyword, Movie, MovieProfile, ProfileField, RefreshSession,
|
||||
UserSettings, WatchEvent, WatchEventSource, WatchlistEntry, WebhookToken,
|
||||
};
|
||||
use crate::ports::{
|
||||
use domain::ports::{
|
||||
GoalCommand, MovieCommand, MovieProfileRepository, RefreshSessionRepository, ReviewRepository,
|
||||
SocialCommand, UserProfileFieldsRepository, UserRepository, UserSettingsRepository,
|
||||
WatchEventCommand, WatchEventQuery, WatchlistRepository, WebhookTokenRepository,
|
||||
};
|
||||
use crate::testing::{
|
||||
use domain::testing::{InMemoryStore, MOVIE_CASCADE_TABLES, USER_CASCADE_TABLES};
|
||||
use domain::testing::{
|
||||
test_goal, test_movie, test_movie_with_external_id, test_review, test_user, test_watched_at,
|
||||
};
|
||||
use crate::value_objects::{ExternalMetadataId, FollowTarget, MovieId, SocialIdentity, UserId};
|
||||
use domain::value_objects::{ExternalMetadataId, FollowTarget, MovieId, SocialIdentity, UserId};
|
||||
|
||||
const REMOTE_ACTOR: &str = "https://remote.example/users/ann";
|
||||
|
||||
@@ -210,7 +210,10 @@ async fn deleting_a_user_leaves_other_users_rows_alone() {
|
||||
assert!(store.find_by_id(bystander.id()).await.unwrap().is_some());
|
||||
}
|
||||
|
||||
async fn seed_movie_with_dependents(store: &Arc<InMemoryStore>, with_review: bool) -> Movie {
|
||||
async fn seed_movie_with_dependents(
|
||||
store: &Arc<InMemoryStore>,
|
||||
with_review: bool,
|
||||
) -> (Movie, UserId) {
|
||||
let user = test_user("alice");
|
||||
UserRepository::save(&**store, &user).await.unwrap();
|
||||
let movie = test_movie("Heat");
|
||||
@@ -240,13 +243,13 @@ async fn seed_movie_with_dependents(store: &Arc<InMemoryStore>, with_review: boo
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
movie
|
||||
(movie, user.id().clone())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_movie_removes_watchlist_entries_and_enrichment_profile() {
|
||||
let store = InMemoryStore::new();
|
||||
let movie = seed_movie_with_dependents(&store, false).await;
|
||||
let (movie, _) = seed_movie_with_dependents(&store, false).await;
|
||||
for table in MOVIE_CASCADE_TABLES {
|
||||
assert!(store.rows_in(table) > 0, "{table} should be seeded");
|
||||
}
|
||||
@@ -262,8 +265,7 @@ async fn deleting_a_movie_removes_watchlist_entries_and_enrichment_profile() {
|
||||
#[tokio::test]
|
||||
async fn deleting_a_movie_detaches_watch_events_instead_of_removing_them() {
|
||||
let store = InMemoryStore::new();
|
||||
let movie = seed_movie_with_dependents(&store, false).await;
|
||||
let user_id = store.watch_events.read().unwrap()[0].user_id().clone();
|
||||
let (movie, user_id) = seed_movie_with_dependents(&store, false).await;
|
||||
|
||||
store.delete_movie(movie.id()).await.unwrap();
|
||||
|
||||
@@ -275,7 +277,7 @@ async fn deleting_a_movie_detaches_watch_events_instead_of_removing_them() {
|
||||
#[tokio::test]
|
||||
async fn deleting_a_movie_that_has_reviews_is_refused_because_reviews_do_not_cascade() {
|
||||
let store = InMemoryStore::new();
|
||||
let movie = seed_movie_with_dependents(&store, true).await;
|
||||
let (movie, _) = seed_movie_with_dependents(&store, true).await;
|
||||
|
||||
let result = store.delete_movie(movie.id()).await;
|
||||
|
||||
@@ -291,11 +293,11 @@ async fn saving_a_user_with_a_taken_username_is_a_validation_error() {
|
||||
UserRepository::save(&*store, &test_user("alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
let clash = crate::models::User::new(
|
||||
crate::value_objects::Email::new("other@example.com".into()).unwrap(),
|
||||
crate::value_objects::Username::new("alice".into()).unwrap(),
|
||||
crate::value_objects::PasswordHash::new("hashed".into()).unwrap(),
|
||||
crate::models::UserRole::Standard,
|
||||
let clash = domain::models::User::new(
|
||||
domain::value_objects::Email::new("other@example.com".into()).unwrap(),
|
||||
domain::value_objects::Username::new("alice".into()).unwrap(),
|
||||
domain::value_objects::PasswordHash::new("hashed".into()).unwrap(),
|
||||
domain::models::UserRole::Standard,
|
||||
);
|
||||
|
||||
let result = UserRepository::save(&*store, &clash).await;
|
||||
@@ -309,11 +311,11 @@ async fn saving_a_user_with_a_taken_email_is_a_validation_error() {
|
||||
UserRepository::save(&*store, &test_user("alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
let clash = crate::models::User::new(
|
||||
crate::value_objects::Email::new("alice@example.com".into()).unwrap(),
|
||||
crate::value_objects::Username::new("alice2".into()).unwrap(),
|
||||
crate::value_objects::PasswordHash::new("hashed".into()).unwrap(),
|
||||
crate::models::UserRole::Standard,
|
||||
let clash = domain::models::User::new(
|
||||
domain::value_objects::Email::new("alice@example.com".into()).unwrap(),
|
||||
domain::value_objects::Username::new("alice2".into()).unwrap(),
|
||||
domain::value_objects::PasswordHash::new("hashed".into()).unwrap(),
|
||||
domain::models::UserRole::Standard,
|
||||
);
|
||||
|
||||
let result = UserRepository::save(&*store, &clash).await;
|
||||
2
crates/domain/tests/value_objects.rs
Normal file
2
crates/domain/tests/value_objects.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "value_objects/value_objects_test.rs"]
|
||||
mod value_objects_test;
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use domain::value_objects::*;
|
||||
|
||||
#[test]
|
||||
fn movie_id_generate_unique() {
|
||||
Reference in New Issue
Block a user