65
crates/domain/src/testing/factories.rs
Normal file
65
crates/domain/src/testing/factories.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use chrono::{DateTime, FixedOffset, NaiveTime, TimeZone, Utc};
|
||||
|
||||
use crate::activity::{Activity, ActivityName, CategoryName};
|
||||
use crate::attachment::ContentType;
|
||||
use crate::entry::{Content, DateRange, Mood, MoodEntry};
|
||||
use crate::reminder::{DaySchedule, Reminder};
|
||||
use crate::user::{Email, PasswordHash, User, UserId, Username};
|
||||
|
||||
pub fn test_user(name: &str) -> User {
|
||||
User::new(
|
||||
Username::new(name).unwrap(),
|
||||
Email::new(format!("{name}@example.com")).unwrap(),
|
||||
PasswordHash::new(format!("hashed:{name}")),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn test_entry(user_id: UserId, mood: Mood) -> MoodEntry {
|
||||
MoodEntry::new(user_id, mood, test_logged_at())
|
||||
}
|
||||
|
||||
pub fn test_entry_days_ago(user_id: UserId, mood: Mood, days_ago: i64) -> MoodEntry {
|
||||
let offset = FixedOffset::east_opt(0).unwrap();
|
||||
let logged_at =
|
||||
offset.from_utc_datetime(&(Utc::now() - chrono::Duration::days(days_ago)).naive_utc());
|
||||
MoodEntry::new(user_id, mood, logged_at)
|
||||
}
|
||||
|
||||
pub fn test_activity(user_id: UserId, name: &str) -> Activity {
|
||||
Activity::new(user_id, ActivityName::new(name).unwrap(), None)
|
||||
}
|
||||
|
||||
pub fn test_activity_with_category(user_id: UserId, name: &str, category: &str) -> Activity {
|
||||
Activity::new(
|
||||
user_id,
|
||||
ActivityName::new(name).unwrap(),
|
||||
Some(CategoryName::new(category).unwrap()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn test_reminder(user_id: UserId) -> Reminder {
|
||||
let time = NaiveTime::from_hms_opt(20, 0, 0).unwrap();
|
||||
Reminder::new(user_id, DaySchedule::every_day_at(time))
|
||||
}
|
||||
|
||||
pub fn test_content(text: &str) -> Content {
|
||||
Content::new(text).unwrap()
|
||||
}
|
||||
|
||||
pub fn test_content_type(mime: &str) -> ContentType {
|
||||
ContentType::new(mime).unwrap()
|
||||
}
|
||||
|
||||
pub fn test_logged_at() -> DateTime<FixedOffset> {
|
||||
FixedOffset::east_opt(3600)
|
||||
.unwrap()
|
||||
.with_ymd_and_hms(2025, 6, 15, 20, 0, 0)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn test_date_range_last_n_days(days: i64) -> DateRange {
|
||||
let offset = FixedOffset::east_opt(0).unwrap();
|
||||
let now = offset.from_utc_datetime(&Utc::now().naive_utc());
|
||||
let start = offset.from_utc_datetime(&(Utc::now() - chrono::Duration::days(days)).naive_utc());
|
||||
DateRange::new(start, now).unwrap()
|
||||
}
|
||||
11
crates/domain/src/testing/fakes/mod.rs
Normal file
11
crates/domain/src/testing/fakes/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod password_hasher;
|
||||
mod store;
|
||||
mod store_activity;
|
||||
mod store_auth;
|
||||
mod store_entry;
|
||||
mod store_infra;
|
||||
mod store_reminder;
|
||||
mod store_user;
|
||||
|
||||
pub use password_hasher::FakePasswordHasher;
|
||||
pub use store::InMemoryStore;
|
||||
14
crates/domain/src/testing/fakes/password_hasher.rs
Normal file
14
crates/domain/src/testing/fakes/password_hasher.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::user::PasswordHash;
|
||||
|
||||
pub struct FakePasswordHasher;
|
||||
|
||||
impl crate::ports::PasswordHasherPort for FakePasswordHasher {
|
||||
fn hash(&self, raw_password: &str) -> Result<PasswordHash, DomainError> {
|
||||
Ok(PasswordHash::new(format!("hashed:{raw_password}")))
|
||||
}
|
||||
|
||||
fn verify(&self, raw_password: &str, hash: &PasswordHash) -> Result<bool, DomainError> {
|
||||
Ok(hash.value() == format!("hashed:{raw_password}"))
|
||||
}
|
||||
}
|
||||
66
crates/domain/src/testing/fakes/store.rs
Normal file
66
crates/domain/src/testing/fakes/store.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::activity::{Activity, ActivityId};
|
||||
use crate::auth::RefreshSession;
|
||||
use crate::entry::{MoodEntry, MoodEntryId};
|
||||
use crate::events::EventEnvelope;
|
||||
use crate::push::{PushSubscription, PushSubscriptionId};
|
||||
use crate::reminder::{Reminder, ReminderId};
|
||||
use crate::user::{User, UserId};
|
||||
|
||||
pub struct InMemoryStore {
|
||||
pub(super) entries: RwLock<HashMap<MoodEntryId, MoodEntry>>,
|
||||
pub(super) activities: RwLock<HashMap<ActivityId, Activity>>,
|
||||
pub(super) users: RwLock<HashMap<UserId, User>>,
|
||||
pub(super) reminders: RwLock<HashMap<ReminderId, Reminder>>,
|
||||
pub refresh_sessions: RwLock<Vec<RefreshSession>>,
|
||||
pub(super) push_subscriptions: RwLock<HashMap<PushSubscriptionId, PushSubscription>>,
|
||||
pub(super) events: RwLock<Vec<EventEnvelope>>,
|
||||
pub(super) sent_reminders: RwLock<Vec<UserId>>,
|
||||
}
|
||||
|
||||
impl InMemoryStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: RwLock::new(HashMap::new()),
|
||||
activities: RwLock::new(HashMap::new()),
|
||||
users: RwLock::new(HashMap::new()),
|
||||
reminders: RwLock::new(HashMap::new()),
|
||||
refresh_sessions: RwLock::new(Vec::new()),
|
||||
push_subscriptions: RwLock::new(HashMap::new()),
|
||||
events: RwLock::new(Vec::new()),
|
||||
sent_reminders: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn published_events(&self) -> Vec<EventEnvelope> {
|
||||
self.events.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn sent_reminders(&self) -> Vec<UserId> {
|
||||
self.sent_reminders.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn entry_count(&self) -> usize {
|
||||
self.entries.read().unwrap().len()
|
||||
}
|
||||
|
||||
pub fn activity_count(&self) -> usize {
|
||||
self.activities.read().unwrap().len()
|
||||
}
|
||||
|
||||
pub fn reminder_count(&self) -> usize {
|
||||
self.reminders.read().unwrap().len()
|
||||
}
|
||||
|
||||
pub fn user_count(&self) -> usize {
|
||||
self.users.read().unwrap().len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
58
crates/domain/src/testing/fakes/store_activity.rs
Normal file
58
crates/domain/src/testing/fakes/store_activity.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use crate::activity::{Activity, ActivityId};
|
||||
use crate::errors::DomainError;
|
||||
use crate::user::UserId;
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ActivityCommandPort for InMemoryStore {
|
||||
async fn save(&self, activity: &Activity) -> Result<(), DomainError> {
|
||||
self.activities
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(activity.id().clone(), activity.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ActivityId) -> Result<(), DomainError> {
|
||||
self.activities.write().unwrap().remove(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_all_by_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.activities
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, a| a.user_id() != user_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ActivityQueryPort for InMemoryStore {
|
||||
async fn find_by_id(&self, id: &ActivityId) -> Result<Option<Activity>, DomainError> {
|
||||
Ok(self.activities.read().unwrap().get(id).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_user(&self, user_id: &UserId) -> Result<Vec<Activity>, DomainError> {
|
||||
Ok(self
|
||||
.activities
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|a| a.user_id() == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_active_by_user(&self, user_id: &UserId) -> Result<Vec<Activity>, DomainError> {
|
||||
Ok(self
|
||||
.activities
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|a| a.user_id() == user_id && !a.is_archived())
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
72
crates/domain/src/testing/fakes/store_auth.rs
Normal file
72
crates/domain/src/testing/fakes/store_auth.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use chrono::{Duration, Utc};
|
||||
|
||||
use crate::auth::{GeneratedToken, RefreshSession};
|
||||
use crate::errors::DomainError;
|
||||
use crate::user::UserId;
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::AuthServicePort for InMemoryStore {
|
||||
async fn generate_token(&self, user_id: &UserId) -> Result<GeneratedToken, DomainError> {
|
||||
let token = format!("fake-jwt-{}", user_id.value());
|
||||
let expires_at = Utc::now() + Duration::hours(1);
|
||||
Ok(GeneratedToken::new(token, expires_at))
|
||||
}
|
||||
|
||||
async fn validate_token(&self, token: &str) -> Result<UserId, DomainError> {
|
||||
let uuid_str = token
|
||||
.strip_prefix("fake-jwt-")
|
||||
.ok_or_else(|| DomainError::Unauthorized("invalid token".into()))?;
|
||||
|
||||
let uuid: uuid::Uuid = uuid_str
|
||||
.parse()
|
||||
.map_err(|_| DomainError::Unauthorized("invalid token".into()))?;
|
||||
|
||||
Ok(UserId::from_uuid(uuid))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::RefreshSessionCommandPort for InMemoryStore {
|
||||
async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> {
|
||||
self.refresh_sessions.write().unwrap().push(session.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn revoke(&self, token: &str) -> Result<(), DomainError> {
|
||||
self.refresh_sessions
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|s| s.token() != token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn revoke_all_for_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.refresh_sessions
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|s| s.user_id() != user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
||||
let mut sessions = self.refresh_sessions.write().unwrap();
|
||||
let before = sessions.len();
|
||||
sessions.retain(|s| !s.is_expired());
|
||||
Ok((before - sessions.len()) as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::RefreshSessionQueryPort for InMemoryStore {
|
||||
async fn find_by_token(&self, token: &str) -> Result<Option<RefreshSession>, DomainError> {
|
||||
Ok(self
|
||||
.refresh_sessions
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|s| s.token() == token)
|
||||
.cloned())
|
||||
}
|
||||
}
|
||||
160
crates/domain/src/testing/fakes/store_entry.rs
Normal file
160
crates/domain/src/testing/fakes/store_entry.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::activity::ActivityId;
|
||||
use crate::entry::{DateRange, Mood, MoodEntry, MoodEntryId};
|
||||
use crate::errors::DomainError;
|
||||
use crate::user::UserId;
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::MoodEntryCommandPort for InMemoryStore {
|
||||
async fn save(&self, entry: &MoodEntry) -> Result<(), DomainError> {
|
||||
self.entries
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(entry.id().clone(), entry.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_batch(&self, entries: &[MoodEntry]) -> Result<(), DomainError> {
|
||||
let mut store = self.entries.write().unwrap();
|
||||
for entry in entries {
|
||||
store.insert(entry.id().clone(), entry.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &MoodEntryId) -> Result<(), DomainError> {
|
||||
self.entries.write().unwrap().remove(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_all_by_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.entries
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, e| e.user_id() != user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_by_date_range(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
range: &DateRange,
|
||||
) -> Result<u64, DomainError> {
|
||||
let mut store = self.entries.write().unwrap();
|
||||
let before = store.len();
|
||||
store.retain(|_, e| {
|
||||
e.user_id() != user_id || e.logged_at() < range.start() || e.logged_at() > range.end()
|
||||
});
|
||||
Ok((before - store.len()) as u64)
|
||||
}
|
||||
|
||||
async fn replace_activity(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
old_activity_id: &ActivityId,
|
||||
new_activity_id: &ActivityId,
|
||||
) -> Result<u64, DomainError> {
|
||||
let mut store = self.entries.write().unwrap();
|
||||
let mut count = 0u64;
|
||||
for entry in store.values_mut() {
|
||||
if entry.user_id() != user_id {
|
||||
continue;
|
||||
}
|
||||
let activities: Vec<_> = entry.activities().to_vec();
|
||||
if activities.contains(old_activity_id) {
|
||||
let replaced: Vec<_> = activities
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
if &a == old_activity_id {
|
||||
new_activity_id.clone()
|
||||
} else {
|
||||
a
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
entry.set_activities(replaced);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::MoodEntryQueryPort for InMemoryStore {
|
||||
async fn find_by_id(&self, id: &MoodEntryId) -> Result<Option<MoodEntry>, DomainError> {
|
||||
Ok(self.entries.read().unwrap().get(id).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_user(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
) -> Result<Vec<MoodEntry>, DomainError> {
|
||||
let mut entries: Vec<_> = self
|
||||
.entries
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|e| e.user_id() == user_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let offset = offset.unwrap_or(0) as usize;
|
||||
let limit = limit.unwrap_or(i64::MAX) as usize;
|
||||
entries = entries.into_iter().skip(offset).take(limit).collect();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn find_by_date_range(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
range: &DateRange,
|
||||
) -> Result<Vec<MoodEntry>, DomainError> {
|
||||
Ok(self
|
||||
.entries
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|e| {
|
||||
e.user_id() == user_id
|
||||
&& e.logged_at() >= range.start()
|
||||
&& e.logged_at() <= range.end()
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_by_mood(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
mood: Mood,
|
||||
) -> Result<Vec<MoodEntry>, DomainError> {
|
||||
Ok(self
|
||||
.entries
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|e| e.user_id() == user_id && e.mood() == mood)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_by_activity(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
activity_id: &ActivityId,
|
||||
) -> Result<Vec<MoodEntry>, DomainError> {
|
||||
Ok(self
|
||||
.entries
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|e| e.user_id() == user_id && e.activities().contains(activity_id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
191
crates/domain/src/testing/fakes/store_infra.rs
Normal file
191
crates/domain/src/testing/fakes/store_infra.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use crate::attachment::{MediaUpload, PhotoId, VoiceMemoId};
|
||||
use crate::entry::{DateRange, MoodEntry};
|
||||
use crate::errors::DomainError;
|
||||
use crate::events::EventEnvelope;
|
||||
use crate::ports::ImportedRow;
|
||||
use crate::push::{PushSubscription, PushSubscriptionId};
|
||||
use crate::user::UserId;
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::EventPublisherPort for InMemoryStore {
|
||||
async fn publish(&self, envelope: EventEnvelope) -> Result<(), DomainError> {
|
||||
self.events.write().unwrap().push(envelope);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::MediaStoragePort for InMemoryStore {
|
||||
async fn store_photo(&self, _upload: MediaUpload) -> Result<PhotoId, DomainError> {
|
||||
Ok(PhotoId::generate())
|
||||
}
|
||||
|
||||
async fn store_voice_memo(&self, _upload: MediaUpload) -> Result<VoiceMemoId, DomainError> {
|
||||
Ok(VoiceMemoId::generate())
|
||||
}
|
||||
|
||||
async fn get_photo(
|
||||
&self,
|
||||
_id: &PhotoId,
|
||||
) -> Result<Option<crate::ports::MediaFile>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_voice_memo(
|
||||
&self,
|
||||
_id: &VoiceMemoId,
|
||||
) -> Result<Option<crate::ports::MediaFile>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn delete_photo(&self, _id: &PhotoId) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_voice_memo(&self, _id: &VoiceMemoId) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::CascadeDeletePort for InMemoryStore {
|
||||
async fn delete_all_user_data(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.entries
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, e| e.user_id() != user_id);
|
||||
self.activities
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, a| a.user_id() != user_id);
|
||||
self.reminders
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, r| r.user_id() != user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_user_account(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.entries
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, e| e.user_id() != user_id);
|
||||
self.activities
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, a| a.user_id() != user_id);
|
||||
self.reminders
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, r| r.user_id() != user_id);
|
||||
self.refresh_sessions
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|s| s.user_id() != user_id);
|
||||
self.push_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, s| s.user_id() != user_id);
|
||||
self.users.write().unwrap().remove(user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_entries_in_range(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
range: &DateRange,
|
||||
) -> Result<Vec<MoodEntry>, DomainError> {
|
||||
let mut entries = self.entries.write().unwrap();
|
||||
let mut removed = Vec::new();
|
||||
entries.retain(|_, e| {
|
||||
if e.user_id() == user_id
|
||||
&& e.logged_at() >= range.start()
|
||||
&& e.logged_at() <= range.end()
|
||||
{
|
||||
removed.push(e.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
Ok(removed)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::PushSubscriptionCommandPort for InMemoryStore {
|
||||
async fn save(&self, subscription: &PushSubscription) -> Result<(), DomainError> {
|
||||
self.push_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(subscription.id().clone(), subscription.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &PushSubscriptionId) -> Result<(), DomainError> {
|
||||
self.push_subscriptions.write().unwrap().remove(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_by_endpoint(&self, endpoint: &str) -> Result<(), DomainError> {
|
||||
self.push_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, s| s.endpoint() != endpoint);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_all_by_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.push_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, s| s.user_id() != user_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::PushSubscriptionQueryPort for InMemoryStore {
|
||||
async fn find_by_user(&self, user_id: &UserId) -> Result<Vec<PushSubscription>, DomainError> {
|
||||
Ok(self
|
||||
.push_subscriptions
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|s| s.user_id() == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_by_endpoint(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
) -> Result<Option<PushSubscription>, DomainError> {
|
||||
Ok(self
|
||||
.push_subscriptions
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.find(|s| s.endpoint() == endpoint)
|
||||
.cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ExportPort for InMemoryStore {
|
||||
async fn export_user_data(
|
||||
&self,
|
||||
_data: &crate::ports::UserExport,
|
||||
) -> Result<Vec<u8>, DomainError> {
|
||||
Ok(b"exported".to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ImportSourcePort for InMemoryStore {
|
||||
async fn read_entries(&self, _data: &[u8]) -> Result<Vec<ImportedRow>, DomainError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
66
crates/domain/src/testing/fakes/store_reminder.rs
Normal file
66
crates/domain/src/testing/fakes/store_reminder.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::reminder::{Reminder, ReminderId};
|
||||
use crate::user::UserId;
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ReminderCommandPort for InMemoryStore {
|
||||
async fn save(&self, reminder: &Reminder) -> Result<(), DomainError> {
|
||||
self.reminders
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(reminder.id().clone(), reminder.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ReminderId) -> Result<(), DomainError> {
|
||||
self.reminders.write().unwrap().remove(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_all_by_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.reminders
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|_, r| r.user_id() != user_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ReminderQueryPort for InMemoryStore {
|
||||
async fn find_by_id(&self, id: &ReminderId) -> Result<Option<Reminder>, DomainError> {
|
||||
Ok(self.reminders.read().unwrap().get(id).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_user(&self, user_id: &UserId) -> Result<Vec<Reminder>, DomainError> {
|
||||
Ok(self
|
||||
.reminders
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|r| r.user_id() == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_all_enabled(&self) -> Result<Vec<Reminder>, DomainError> {
|
||||
Ok(self
|
||||
.reminders
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|r| r.is_enabled())
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::ReminderSenderPort for InMemoryStore {
|
||||
async fn send_reminder(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
self.sent_reminders.write().unwrap().push(user_id.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
47
crates/domain/src/testing/fakes/store_user.rs
Normal file
47
crates/domain/src/testing/fakes/store_user.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::user::{Email, User, UserId, Username};
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::UserCommandPort for InMemoryStore {
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||
self.users
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(user.id().clone(), user.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &UserId) -> Result<(), DomainError> {
|
||||
self.users.write().unwrap().remove(id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::ports::UserQueryPort for InMemoryStore {
|
||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||
Ok(self.users.read().unwrap().get(id).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
|
||||
Ok(self
|
||||
.users
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.find(|u| u.username() == username)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
||||
Ok(self
|
||||
.users
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.find(|u| u.email() == email)
|
||||
.cloned())
|
||||
}
|
||||
}
|
||||
5
crates/domain/src/testing/mod.rs
Normal file
5
crates/domain/src/testing/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod factories;
|
||||
mod fakes;
|
||||
|
||||
pub use factories::*;
|
||||
pub use fakes::{FakePasswordHasher, InMemoryStore};
|
||||
Reference in New Issue
Block a user