refactor: 5 architectural improvements (Tasks 2-5 + Task 6 fix)
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m19s
test / integration (pull_request) Failing after 17m15s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m19s
test / integration (pull_request) Failing after 17m15s
- feat(domain): Hashtag value object with canonical extract() — unifies two divergent private implementations; fields pre-compute raw/normalized/url_slug/ap_name - feat(presentation): Deps<S: FromAppState> extractor — each handler now declares its exact dependency surface; AppState unchanged; handlers become unit-testable without mocking all 20 deps - refactor(feed): replace 5 flat FeedRepository methods with FeedQuery/FeedScope — single query() method; SQL shared logic lives once; adding feed types no longer requires 5 edits - refactor(activitypub): ActivityPubRepository + OutboundFederationPort moved out of domain::ports into activitypub-base::ap_ports — domain crate no longer knows about AP IDs, inboxes, or actor URLs - fix(outbox): OutboxRelay now opens a per-row transaction so FOR UPDATE SKIP LOCKED actually holds the lock during publish + mark_delivered
This commit is contained in:
150
crates/application/src/testing.rs
Normal file
150
crates/application/src/testing.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
/// Test helpers for application-layer tests that need activitypub_base traits.
|
||||
use activitypub_base::{ActivityPubRepository, ActorApUrls, OutboxEntry};
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::user::User,
|
||||
testing::TestStore,
|
||||
value_objects::{Email, PasswordHash, ThoughtId, UserId, Username},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Extends `TestStore` with AP-specific lookup maps.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TestApRepo {
|
||||
pub inner: TestStore,
|
||||
/// UserId → ActorApUrls (for get_actor_ap_urls)
|
||||
pub actor_ap_urls: Arc<Mutex<HashMap<UserId, ActorApUrls>>>,
|
||||
}
|
||||
|
||||
impl TestApRepo {
|
||||
pub fn new(inner: TestStore) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
actor_ap_urls: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActivityPubRepository for TestApRepo {
|
||||
async fn outbox_entries_for_actor(
|
||||
&self,
|
||||
_uid: &UserId,
|
||||
) -> Result<Vec<OutboxEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn outbox_page_for_actor(
|
||||
&self,
|
||||
_uid: &UserId,
|
||||
_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
_limit: usize,
|
||||
) -> Result<Vec<OutboxEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn find_remote_actor_id(
|
||||
&self,
|
||||
actor_ap_url: &str,
|
||||
) -> Result<Option<UserId>, DomainError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.actor_ap_ids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(actor_ap_url)
|
||||
.cloned())
|
||||
}
|
||||
async fn intern_remote_actor(&self, actor_ap_url: &str) -> Result<UserId, DomainError> {
|
||||
if let Some(uid) = self.find_remote_actor_id(actor_ap_url).await? {
|
||||
return Ok(uid);
|
||||
}
|
||||
let uid = UserId::new();
|
||||
let handle = url::Url::parse(actor_ap_url)
|
||||
.map(|u| u.path().trim_start_matches('/').replace('/', "_"))
|
||||
.unwrap_or_else(|_| format!("remote_{}", &uid.to_string()[..8]));
|
||||
let user = User {
|
||||
id: uid.clone(),
|
||||
username: Username::from_trusted(handle),
|
||||
email: Email::from_trusted(format!("{}@remote", uid)),
|
||||
password_hash: PasswordHash("".into()),
|
||||
display_name: None,
|
||||
bio: None,
|
||||
avatar_url: None,
|
||||
header_url: None,
|
||||
custom_css: None,
|
||||
local: false,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
self.inner.users.lock().unwrap().push(user);
|
||||
self.inner
|
||||
.actor_ap_ids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(actor_ap_url.to_string(), uid.clone());
|
||||
Ok(uid)
|
||||
}
|
||||
async fn update_remote_actor_display(
|
||||
&self,
|
||||
_user_id: &UserId,
|
||||
_display_name: Option<&str>,
|
||||
_avatar_url: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn accept_note(
|
||||
&self,
|
||||
_ap_id: &str,
|
||||
_author_id: &UserId,
|
||||
_content: &str,
|
||||
_published: chrono::DateTime<chrono::Utc>,
|
||||
_sensitive: bool,
|
||||
_content_warning: Option<String>,
|
||||
_visibility: &str,
|
||||
_in_reply_to: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn apply_note_update(
|
||||
&self,
|
||||
_ap_id: &str,
|
||||
_new_content: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn retract_actor_notes(&self, _actor_ap_url: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn count_local_notes(&self) -> Result<u64, DomainError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.thoughts
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|t| t.local)
|
||||
.count() as u64)
|
||||
}
|
||||
async fn get_thought_ap_id(
|
||||
&self,
|
||||
thought_id: &ThoughtId,
|
||||
) -> Result<Option<String>, DomainError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.thought_ap_ids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(thought_id)
|
||||
.cloned())
|
||||
}
|
||||
async fn get_actor_ap_urls(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<ActorApUrls>, DomainError> {
|
||||
Ok(self.actor_ap_urls.lock().unwrap().get(user_id).cloned())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user