Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
[package]
|
||||
name = "sqlite-federation"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
sqlx = { workspace = true }
|
||||
activitypub = { workspace = true }
|
||||
adapter-common = { workspace = true }
|
||||
k-ap = { version = "0.5.0", registry = "gitea" }
|
||||
sqlite-social = { workspace = true }
|
||||
k-ap = { workspace = true }
|
||||
sqlite = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4", "serde"] }
|
||||
chrono = { workspace = true, features = ["clock", "serde"] }
|
||||
tracing = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
domain = { workspace = true, features = ["test-helpers"] }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ use chrono::Utc;
|
||||
use k_ap::ActivityRepository;
|
||||
|
||||
use super::SqliteFederationRepository;
|
||||
use adapter_common::datetime_to_str;
|
||||
use sqlite::persistence::datetime_to_str;
|
||||
|
||||
#[async_trait]
|
||||
impl ActivityRepository for SqliteFederationRepository {
|
||||
|
||||
@@ -5,7 +5,7 @@ use k_ap::{AnnounceRepository, Keypair, KeypairRepository, RemoteActor, RemoteAc
|
||||
use sqlx::Row;
|
||||
|
||||
use super::{SqliteFederationRepository, remote_actor_from_row};
|
||||
use adapter_common::datetime_to_str;
|
||||
use sqlite::persistence::datetime_to_str;
|
||||
|
||||
#[async_trait]
|
||||
impl KeypairRepository for SqliteFederationRepository {
|
||||
|
||||
@@ -5,7 +5,7 @@ use k_ap::{ActorBlocklist, BlockedDomain, DomainBlocklist};
|
||||
use sqlx::Row;
|
||||
|
||||
use super::SqliteFederationRepository;
|
||||
use adapter_common::datetime_to_str;
|
||||
use sqlite::persistence::datetime_to_str;
|
||||
|
||||
#[async_trait]
|
||||
impl DomainBlocklist for SqliteFederationRepository {
|
||||
|
||||
@@ -5,7 +5,7 @@ use k_ap::{Follower, FollowerReader, FollowerStatus, FollowerWriter, RemoteActor
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::{SqliteFederationRepository, remote_actor_from_row, status_to_str, str_to_status};
|
||||
use adapter_common::datetime_to_str;
|
||||
use sqlite::persistence::datetime_to_str;
|
||||
|
||||
#[async_trait]
|
||||
impl FollowerWriter for SqliteFederationRepository {
|
||||
|
||||
@@ -4,7 +4,7 @@ use chrono::Utc;
|
||||
use k_ap::{FollowingReader, FollowingStatus, FollowingWriter, RemoteActor, RemoteActorCache};
|
||||
|
||||
use crate::{SqliteFederationRepository, remote_actor_from_row};
|
||||
use adapter_common::datetime_to_str;
|
||||
use sqlite::persistence::datetime_to_str;
|
||||
|
||||
#[async_trait]
|
||||
impl FollowingWriter for SqliteFederationRepository {
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn wire(
|
||||
instance: domain::value_objects::InstanceIdentity,
|
||||
) -> activitypub::FederationRepos {
|
||||
let fed = std::sync::Arc::new(SqliteFederationRepository::new(pool.clone()));
|
||||
let social = std::sync::Arc::new(sqlite_social::SqliteSocialRepository::new(pool, instance));
|
||||
let social = std::sync::Arc::new(sqlite::SqliteSocialRepository::new(pool, instance));
|
||||
activitypub::FederationRepos {
|
||||
activity: std::sync::Arc::clone(&fed) as _,
|
||||
follow: std::sync::Arc::clone(&fed) as _,
|
||||
@@ -92,15 +92,3 @@ pub fn wire(
|
||||
follow_query: social as _,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/actor_block_tests.rs"]
|
||||
mod actor_block_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/domain_block_tests.rs"]
|
||||
mod domain_block_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/lib.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
use activitypub::{RemoteReviewRepository, RemoteReviewUpdate};
|
||||
use activitypub::{NewRemoteReview, RemoteReviewRepository, RemoteReviewUpdate};
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use domain::models::{Review, ReviewSource};
|
||||
use domain::models::ReviewSource;
|
||||
|
||||
use super::SqliteFederationRepository;
|
||||
use adapter_common::datetime_to_str;
|
||||
use sqlite::persistence::{datetime_to_str, enqueue_events};
|
||||
|
||||
#[async_trait]
|
||||
impl RemoteReviewRepository for SqliteFederationRepository {
|
||||
async fn save_remote_review(
|
||||
&self,
|
||||
review: &Review,
|
||||
ap_id: &str,
|
||||
movie_title: &str,
|
||||
release_year: u16,
|
||||
external_metadata_id: Option<&str>,
|
||||
poster_url: Option<&str>,
|
||||
) -> Result<()> {
|
||||
async fn save_remote_review(&self, new: NewRemoteReview<'_>) -> Result<()> {
|
||||
let NewRemoteReview {
|
||||
review,
|
||||
ap_id,
|
||||
movie_title,
|
||||
release_year,
|
||||
external_metadata_id,
|
||||
poster_url,
|
||||
events,
|
||||
} = new;
|
||||
let actor_url = match review.source() {
|
||||
ReviewSource::Remote { actor_url } => actor_url.clone(),
|
||||
ReviewSource::Local => {
|
||||
@@ -24,6 +25,7 @@ impl RemoteReviewRepository for SqliteFederationRepository {
|
||||
}
|
||||
};
|
||||
let movie_id = review.movie_id().value().to_string();
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||
VALUES (?, ?, ?, ?, NULL, ?)
|
||||
@@ -36,7 +38,7 @@ impl RemoteReviewRepository for SqliteFederationRepository {
|
||||
.bind(movie_title)
|
||||
.bind(release_year.max(1888) as i64)
|
||||
.bind(poster_url)
|
||||
.execute(&self.pool)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let id = review.id().value().to_string();
|
||||
@@ -59,8 +61,10 @@ impl RemoteReviewRepository for SqliteFederationRepository {
|
||||
.bind(&actor_url)
|
||||
.bind(ap_id)
|
||||
.bind(review.spoilers())
|
||||
.execute(&self.pool)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
enqueue_events(&mut tx, events).await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
8
crates/adapters/sqlite-federation/tests/federation.rs
Normal file
8
crates/adapters/sqlite-federation/tests/federation.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[path = "federation/actor_block_test.rs"]
|
||||
mod actor_block_test;
|
||||
#[path = "federation/announce_test.rs"]
|
||||
mod announce_test;
|
||||
#[path = "federation/domain_block_test.rs"]
|
||||
mod domain_block_test;
|
||||
#[path = "federation/remote_review_test.rs"]
|
||||
mod remote_review_test;
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use k_ap::ActorBlocklist;
|
||||
use sqlite_federation::SqliteFederationRepository;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
async fn test_pool() -> SqlitePool {
|
||||
@@ -25,7 +25,7 @@ async fn test_pool() -> SqlitePool {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn block_and_check_actor() {
|
||||
async fn blocked_actor_is_detected_listed_and_unblocked() {
|
||||
let pool = test_pool().await;
|
||||
let user_id = uuid::Uuid::parse_str(
|
||||
&sqlx::query_scalar::<_, String>("SELECT id FROM users LIMIT 1")
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
use k_ap::AnnounceRepository;
|
||||
use sqlite_federation::SqliteFederationRepository;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
async fn test_pool() -> SqlitePool {
|
||||
@@ -11,7 +11,7 @@ async fn test_pool() -> SqlitePool {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_announce_stores_and_counts() {
|
||||
async fn added_announce_is_counted() {
|
||||
let pool = test_pool().await;
|
||||
let repo = SqliteFederationRepository::new(pool);
|
||||
repo.add_announce(
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use k_ap::DomainBlocklist;
|
||||
use sqlite_federation::SqliteFederationRepository;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
async fn test_pool() -> SqlitePool {
|
||||
@@ -0,0 +1,113 @@
|
||||
use activitypub::{NewRemoteReview, RemoteReviewRepository};
|
||||
use domain::{
|
||||
events::DomainEvent,
|
||||
models::{PersistedReview, Review, ReviewSource},
|
||||
value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
|
||||
};
|
||||
use sqlite_federation::SqliteFederationRepository;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
const ACTOR: &str = "https://remote.example/users/ann";
|
||||
const AP_ID: &str = "https://remote.example/reviews/1";
|
||||
|
||||
async fn migrated_pool() -> SqlitePool {
|
||||
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
.unwrap();
|
||||
sqlite::migrate(&pool).await.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn count(pool: &SqlitePool, table: &str) -> i64 {
|
||||
sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table}"))
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn remote_review(movie_id: &MovieId) -> Review {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
Review::from_persistence(PersistedReview {
|
||||
id: ReviewId::generate(),
|
||||
movie_id: movie_id.clone(),
|
||||
user_id: UserId::generate(),
|
||||
rating: Rating::new(4).unwrap(),
|
||||
comment: Some(Comment::new("good".into()).unwrap()),
|
||||
watched_at: now,
|
||||
created_at: now,
|
||||
source: ReviewSource::Remote {
|
||||
actor_url: ACTOR.into(),
|
||||
},
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn enrichment_requested(movie_id: &MovieId) -> DomainEvent {
|
||||
DomainEvent::MovieEnrichmentRequested {
|
||||
movie_id: movie_id.clone(),
|
||||
external_metadata_id: ExternalMetadataId::new("tmdb:603".into()).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save(pool: &SqlitePool, review: &Review, events: &[DomainEvent]) -> anyhow::Result<()> {
|
||||
SqliteFederationRepository::new(pool.clone())
|
||||
.save_remote_review(NewRemoteReview {
|
||||
review,
|
||||
ap_id: AP_ID,
|
||||
movie_title: "The Matrix",
|
||||
release_year: 1999,
|
||||
external_metadata_id: Some("tmdb:603"),
|
||||
poster_url: None,
|
||||
events,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn saving_a_remote_review_writes_it_and_its_event_in_one_commit() {
|
||||
let pool = migrated_pool().await;
|
||||
let movie_id = MovieId::generate();
|
||||
let review = remote_review(&movie_id);
|
||||
|
||||
save(&pool, &review, &[enrichment_requested(&movie_id)])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count(&pool, "reviews").await, 1);
|
||||
assert_eq!(count(&pool, "movies").await, 1);
|
||||
assert_eq!(count(&pool, "event_queue").await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_failing_enqueue_rolls_back_the_remote_review_and_its_movie() {
|
||||
let pool = migrated_pool().await;
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER fail_event_insert BEFORE INSERT ON event_queue \
|
||||
BEGIN SELECT RAISE(ABORT, 'enqueue failed'); END",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let movie_id = MovieId::generate();
|
||||
let review = remote_review(&movie_id);
|
||||
|
||||
let result = save(&pool, &review, &[enrichment_requested(&movie_id)]).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(count(&pool, "reviews").await, 0);
|
||||
assert_eq!(count(&pool, "movies").await, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_remote_review_without_events_enqueues_nothing() {
|
||||
let pool = migrated_pool().await;
|
||||
let movie_id = MovieId::generate();
|
||||
|
||||
save(&pool, &remote_review(&movie_id), &[]).await.unwrap();
|
||||
|
||||
assert_eq!(count(&pool, "reviews").await, 1);
|
||||
assert_eq!(count(&pool, "event_queue").await, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user