strip session-added comments
Some checks failed
CI / Check / Test (push) Has been cancelled
CI / Check / Test (pull_request) Has been cancelled

This commit is contained in:
2026-10-26 10:32:38 +01:00
parent a270632e87
commit f020108024
138 changed files with 0 additions and 719 deletions

View File

@@ -16,9 +16,6 @@ use tokio::sync::{Mutex, mpsc};
use crate::shared::enqueue_events;
pub(crate) use payload::DbEventPayload;
/// How long a claim holds a row. A worker that dies between claim and
/// ack/nack strands its rows in `processing`; once the lease lapses the claim
/// query takes them again. Must exceed the slowest handler.
pub(crate) const CLAIM_LEASE_SECONDS: i64 = 600;
pub struct DbEventQueueConfig {
@@ -140,9 +137,6 @@ struct QueueRow {
claimed_at: Option<String>,
}
/// Claims a batch in one atomic statement. A read-then-write transaction can
/// fail with SQLITE_BUSY_SNAPSHOT (517) when another connection commits in
/// between, which `busy_timeout` cannot retry.
async fn claim_batch(
pool: &SqlitePool,
config: &DbEventQueueConfig,
@@ -168,13 +162,10 @@ async fn claim_batch(
.await
.map_err(|e| DomainError::InfrastructureError(format!("claim batch: {e}")))?;
// RETURNING order is unspecified; deliver oldest first.
rows.sort_by(|a, b| (&a.next_attempt_at, a.id).cmp(&(&b.next_attempt_at, b.id)));
Ok(rows)
}
/// A row that cannot be decoded can never succeed, and has no ack handle to
/// settle it with; park it so it is neither retried forever nor left claimed.
async fn dead_letter_undecodable(pool: &SqlitePool, row_id: i64, error: &DomainError) {
tracing::error!(
row_id,
@@ -223,14 +214,10 @@ struct DbAckHandle {
row_id: i64,
attempts: i32,
max_attempts: i32,
/// The claim this handle belongs to. Settling is conditional on it, so a
/// worker whose lease lapsed and was reclaimed cannot flip the new claim's row.
claimed_at: Option<String>,
}
impl DbAckHandle {
/// Runs a settle statement whose last two binds are the row id and claim
/// stamp. Zero rows changed means the claim is no longer ours.
async fn settle<'q>(
&self,
what: &str,

View File

@@ -1,7 +1,3 @@
//! Inline (not under `tests/`) because `claim_batch` is private and the race
//! only shows on its claim path; two pools on a file-backed WAL database are
//! needed, as in production.
use std::str::FromStr;
use sqlx::SqlitePool;
@@ -73,7 +69,6 @@ async fn claiming_while_another_connection_writes_does_not_fail() {
.await
.expect("claim must not fail while another connection writes");
if batch.is_empty() {
// an empty UPDATE still takes the write lock; don't starve the writer
tokio::task::yield_now().await;
}
claimed += batch.len();
@@ -315,8 +310,6 @@ mod stale_handle {
use super::{claim_batch, config};
use crate::event_queue::{CLAIM_LEASE_SECONDS, SqliteEventQueue};
/// Claims a row, lets its lease lapse, and has another worker reclaim it.
/// Returns the first (now stale) worker's envelope.
async fn reclaimed() -> (sqlx::SqlitePool, EventEnvelope) {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
@@ -340,8 +333,6 @@ mod stale_handle {
.await
.unwrap();
assert_eq!(claim_batch(&pool, &config(10)).await.unwrap().len(), 1);
// Real reclaims happen a lease later, so their timestamp differs from
// the stale handle's; the claim clock has only second resolution.
let second_claim = (chrono::Utc::now() - chrono::Duration::seconds(5))
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string();

View File

@@ -20,8 +20,6 @@ pub use repositories::{
create_federated_profile_query, create_image_ref, create_person_adapter, create_search_adapter,
};
/// Row helpers shared with the `sqlite-federation` crate (which cannot live
/// here because it depends on `activitypub`/`k-ap`).
pub mod persistence {
pub use crate::shared::{datetime_to_str, enqueue_events};
}
@@ -79,7 +77,6 @@ pub async fn wire(database_url: &str) -> anyhow::Result<SqliteWireOutput> {
wire_pool(pool).await
}
/// Migrates an already-connected pool and builds every repository on it.
pub async fn wire_pool(pool: SqlitePool) -> anyhow::Result<SqliteWireOutput> {
use anyhow::Context;

View File

@@ -3,10 +3,6 @@ use std::time::Duration;
use domain::errors::DomainError;
use sqlx::SqlitePool;
/// Server and worker both migrate at startup, so two processes can race on the
/// same database. The loser's migration fails (e.g. duplicate column) because
/// the winner already applied it; re-running re-reads `_sqlx_migrations` and
/// skips what is done. Only an error that persists across all retries is returned.
const RETRY_BACKOFFS: [Duration; 5] = [
Duration::from_millis(100),
Duration::from_millis(250),

View File

@@ -9,10 +9,6 @@ use crate::shared::map_sqlx_error;
const TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
/// Every table the purge empties for the account, with the column that holds
/// the user id. `reviews` is further restricted to the user's own (non-remote)
/// rows. A guard test compares this list with the schema, so a new
/// user-owned table cannot be forgotten.
pub const PURGED_USER_TABLES: &[(&str, &str)] = &[
("reviews", "user_id"),
("watchlist_entries", "user_id"),
@@ -30,13 +26,8 @@ pub const PURGED_USER_TABLES: &[(&str, &str)] = &[
("blocked_actors", "local_user_id"),
];
/// Tables that reference a user but are kept until the tombstone cleanup:
/// the keypair is needed to sign queued federation deliveries.
pub const RETAINED_USER_TABLES: &[&str] = &["ap_local_actors"];
/// Columns that identify the user by actor or content URL rather than by id.
/// The purge matches rows on them; the guard test asserts each still exists so
/// a rename cannot silently stop the purge from clearing them.
pub const URL_KEYED_USER_COLUMNS: &[(&str, &str)] = &[
("ap_announces", "object_url"),
("ap_announces", "actor_url"),
@@ -44,9 +35,6 @@ pub const URL_KEYED_USER_COLUMNS: &[(&str, &str)] = &[
("ap_following", "remote_actor_url"),
];
/// Columns holding serialized events and activities that carry the user's id
/// (the outbox log keeps finished rows, including authored activity JSON) and
/// the global wrap-up reports, whose `most_active_user` names a member.
pub const PAYLOAD_KEYED_USER_COLUMNS: &[(&str, &str)] = &[
("event_queue", "payload"),
("wrap_up_records", "report_json"),
@@ -95,8 +83,6 @@ impl AccountPurger for SqliteAccountPurger {
let base = self.instance.base_url().to_string();
let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?;
// Strip first: it claims the purge. Only a tombstoned, not yet purged
// account matches, so a live account (or a re-run) changes nothing.
let claimed = sqlx::query(
"UPDATE users SET email = 'deleted-' || id || '@deleted.invalid',
password_hash = '!purged', display_name = NULL, bio = NULL,
@@ -113,7 +99,6 @@ impl AccountPurger for SqliteAccountPurger {
return Ok(());
}
// Announces of the user's content, matched before the reviews go.
sqlx::query(
"DELETE FROM ap_announces
WHERE object_url IN (
@@ -130,11 +115,6 @@ impl AccountPurger for SqliteAccountPurger {
.await
.map_err(map_sqlx_error)?;
// The outbox log: finished events and activities authored as, or
// mentioning, this user. Unfinished rows are never touched (they
// belong to other users' work too and finish or dead-letter on their
// own); the tombstone cleanup removes their history later. The
// `UserDeleted` row that triggered this run stays until then as well.
sqlx::query(
"DELETE FROM event_queue
WHERE instr(payload, ?1) > 0
@@ -146,8 +126,6 @@ impl AccountPurger for SqliteAccountPurger {
.await
.map_err(map_sqlx_error)?;
// Global wrap-up reports name their most active member; redact it
// (the type allows none) and keep the rest of the report.
sqlx::query(
"UPDATE wrap_up_records
SET report_json = json_set(report_json, '$.most_active_user', NULL)
@@ -159,7 +137,6 @@ impl AccountPurger for SqliteAccountPurger {
.await
.map_err(map_sqlx_error)?;
// Other accounts' follows of this one.
for table in ["ap_followers", "ap_following"] {
sqlx::query(&format!("DELETE FROM {table} WHERE remote_actor_url = ?"))
.bind(&actor_url)
@@ -201,9 +178,6 @@ impl AccountPurger for SqliteAccountPurger {
) -> Result<u64, DomainError> {
let cutoff = cutoff.format(TIMESTAMP_FORMAT).to_string();
let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?;
// A tombstone whose id is still named by an unfinished event waits for
// the next run: that event may yet be handled, and its history row
// must be erased with the rest.
let expiring: Vec<String> = sqlx::query_scalar(
"SELECT id FROM users u
WHERE purged_at IS NOT NULL AND purged_at < ?
@@ -217,15 +191,11 @@ impl AccountPurger for SqliteAccountPurger {
.await
.map_err(map_sqlx_error)?;
for id in &expiring {
// Includes the `UserDeleted` row and the `Delete` deliveries.
sqlx::query("DELETE FROM event_queue WHERE instr(payload, ?) > 0")
.bind(id)
.execute(&mut *tx)
.await
.map_err(map_sqlx_error)?;
// A Follow arriving on the shared inbox after the purge can leave
// a late row for the tombstone; delete it explicitly rather than
// rely on the foreign-key cascade of the final `users` delete.
for table in ["ap_followers", "ap_following"] {
sqlx::query(&format!("DELETE FROM {table} WHERE local_user_id = ?"))
.bind(id)

View File

@@ -131,8 +131,6 @@ impl DiaryQuery for SqliteDiaryRepository {
FeedSortBy::RatingAsc => "r.rating ASC, r.watched_at ASC",
};
// Tombstoned local authors vanish from the feed; remote and orphan
// rows have no `users` match, so `deleted_at` is NULL for them.
where_parts.push("u.deleted_at IS NULL".to_string());
let where_clause = where_parts.join(" AND ");

View File

@@ -26,8 +26,6 @@ pub fn create_image_ref(pool: SqlitePool) -> (Arc<dyn ImageRefCommand>, Arc<dyn
#[async_trait]
impl ImageRefCommand for SqliteImageRefAdapter {
// Tombstoned users (`deleted_at` set) are intentionally included: their
// avatar stays referenced, and so protected from GC, until the purge.
async fn swap(&self, old_key: &str, new_key: &str) -> Result<(), DomainError> {
let mut tx = self
.pool
@@ -54,8 +52,6 @@ impl ImageRefCommand for SqliteImageRefAdapter {
#[async_trait]
impl ImageRefQuery for SqliteImageRefAdapter {
// Tombstoned users are intentionally visible here until the purge removes
// the row, so their avatar is not garbage-collected early.
async fn list_keys(&self) -> Result<Vec<String>, DomainError> {
let rows: Vec<(String,)> = sqlx::query_as(
"SELECT avatar_path FROM users WHERE avatar_path IS NOT NULL

View File

@@ -37,8 +37,6 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
.await
.map_err(crate::shared::map_sqlx_error)?;
// The old row still holds the external id (it is deleted last); release it
// so the canonical upsert does not violate UNIQUE(external_metadata_id).
sqlx::query("UPDATE movies SET external_metadata_id = NULL WHERE id = ?")
.bind(&old)
.execute(&mut *tx)

View File

@@ -6,7 +6,6 @@ use domain::{
};
use sqlx::SqlitePool;
/// Matches the column default in migration 0101, so string comparison orders rows by time.
const TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
const CLAIM_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
@@ -23,8 +22,6 @@ impl SqliteProcessedEventLedger {
)
}
/// A shorter lease than production's, so lease takeover can be tested
/// without waiting half an hour.
pub fn with_lease(pool: SqlitePool, lease: std::time::Duration) -> Self {
Self {
pool,
@@ -37,7 +34,6 @@ fn stamp(at: chrono::NaiveDateTime) -> String {
at.format(TIMESTAMP_FORMAT).to_string()
}
/// Millisecond precision, so two claims taken in one second have distinct tokens.
fn claim_stamp(at: chrono::NaiveDateTime) -> String {
at.format(CLAIM_FORMAT).to_string()
}
@@ -51,13 +47,9 @@ fn parse_claim_stamp(value: &str) -> Option<chrono::NaiveDateTime> {
#[async_trait]
impl ProcessedEventLedger for SqliteProcessedEventLedger {
async fn begin(&self, id: &EventId, handler: &str) -> Result<Begin, DomainError> {
// Retried once: the row can vanish (released) between the claim attempt
// and the state lookup.
for _ in 0..2 {
let now = chrono::Utc::now().naive_utc();
let started = claim_stamp(now);
// One statement, so two workers cannot both win: insert if absent,
// or take over an in_progress claim past its lease.
let claimed = sqlx::query(
"INSERT INTO processed_events (event_id, handler, status, started_at, processed_at)
VALUES (?, ?, 'in_progress', ?, ?)
@@ -214,7 +206,6 @@ mod tests {
token(ledger.begin(&id, "b").await.unwrap());
token(ledger.begin(&EventId::new(), "a").await.unwrap());
// The blocked delivery is told to come back when the lease runs out.
let lease = std::time::Duration::from_secs(HANDLER_LEASE_SECONDS as u64);
assert!(retry_after <= lease && retry_after > lease - std::time::Duration::from_secs(5));
}
@@ -260,7 +251,6 @@ mod tests {
insert(&pool, &id, "in_progress", "2020-01-01T00:00:00Z").await;
token(ledger.begin(&id, "h").await.unwrap());
// The takeover refreshed the lease, so a third delivery is blocked.
blocked(ledger.begin(&id, "h").await.unwrap());
let processed_at: String = sqlx::query_scalar("SELECT processed_at FROM processed_events")
.fetch_one(&pool)
@@ -295,7 +285,6 @@ mod tests {
ledger.release(&id, "a", &lapsed).await.unwrap();
ledger.complete(&id, "a", &lapsed).await.unwrap();
// Still the new owner's live claim: neither call touched it.
blocked(ledger.begin(&id, "a").await.unwrap());
ledger.complete(&id, "a", &owner).await.unwrap();
assert_eq!(ledger.begin(&id, "a").await.unwrap(), Begin::AlreadyDone);

View File

@@ -169,9 +169,6 @@ impl UserRepository for SqliteUserRepository {
.begin()
.await
.map_err(crate::shared::map_sqlx_error)?;
// The last-admin guard lives in this single statement, so two admins
// deleting each other concurrently cannot both pass it: the write lock
// serialises them and the loser sees the winner's tombstone.
let stamped = sqlx::query(
"UPDATE users SET deleted_at = ?1 WHERE id = ?2 AND deleted_at IS NULL
AND (role != 'admin' OR EXISTS (
@@ -283,8 +280,6 @@ impl UserRepository for SqliteUserRepository {
}
impl SqliteUserRepository {
/// Uniqueness check over every row, tombstones included: a deleted
/// account keeps its email and username until the purge removes the row.
async fn row_exists(&self, column: &str, value: &str) -> Result<bool, DomainError> {
sqlx::query_scalar::<_, i64>(&format!(
"SELECT EXISTS(SELECT 1 FROM users WHERE {column} = ?)"

View File

@@ -52,7 +52,6 @@ impl WatchlistRepository for SqliteWatchlistRepository {
.map_err(crate::shared::map_sqlx_error)?
.rows_affected()
> 0;
// A repeat add changed nothing, so it announces nothing.
if inserted {
enqueue_events(&mut tx, events).await?;
}

View File

@@ -10,9 +10,6 @@ use domain::{
},
};
/// THE outbox write path: inserts one `event_queue` row per event, each with a
/// fresh `EventId`, inside the caller's transaction so a change and the events
/// announcing it commit or roll back together.
pub async fn enqueue_events(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
events: &[domain::events::DomainEvent],

View File

@@ -1,6 +1,3 @@
//! The purge removes every row a deleted account owns, strips the user row to
//! a data-free tombstone, and the cleanup later drops the tombstone itself.
use std::collections::BTreeSet;
use std::str::FromStr;
@@ -69,8 +66,6 @@ struct Seeded {
movie_id: String,
}
/// Alice owns a row in every table; bob has a little of everything, including
/// a follow of alice and an announce of his own review, which must survive.
async fn seed(pool: &SqlitePool, parts: &sqlite::SqliteWireOutput) -> Seeded {
let alice = test_user("alice");
let bob = test_user("bob");
@@ -205,7 +200,6 @@ async fn seed(pool: &SqlitePool, parts: &sqlite::SqliteWireOutput) -> Seeded {
follower_actor_url: REMOTE_ACTOR.into(),
status: FollowStatus::Accepted,
},
// bob follows alice, and alice follows bob
FollowWrite::AddFollow {
follower_id: b.value(),
target_actor_url: alice_actor.clone(),
@@ -221,7 +215,6 @@ async fn seed(pool: &SqlitePool, parts: &sqlite::SqliteWireOutput) -> Seeded {
)
.await
.unwrap();
// A remote review attributed to the same account id is not the user's.
exec(
pool,
"INSERT INTO reviews (id, movie_id, user_id, rating, comment, watched_at, created_at, remote_actor_url)
@@ -234,7 +227,6 @@ async fn seed(pool: &SqlitePool, parts: &sqlite::SqliteWireOutput) -> Seeded {
],
)
.await;
// Announces of alice's review and goal, of bob's review, and one by alice.
let alice_review: String =
sqlx::query_scalar("SELECT id FROM reviews WHERE user_id = ? AND remote_actor_url IS NULL")
.bind(&a_str)
@@ -352,7 +344,6 @@ async fn purge_removes_every_user_owned_row() {
purger(&pool).purge(&s.user).await.unwrap();
for (table, column) in PURGED_USER_TABLES {
// Reviews: only the remote one, asserted below, is left.
let left = if *table == "reviews" { 1 } else { 0 };
assert_eq!(
count_for(&pool, table, column, &s.user).await,
@@ -406,14 +397,12 @@ async fn purge_removes_every_user_owned_row() {
"finished rows mentioning the user are gone; pending/processing rows (incl. other users' events about her) and bob's own rows stay"
);
// The other account is untouched.
for (table, column) in PURGED_USER_TABLES {
assert!(
count_for(&pool, table, column, &s.other).await > 0,
"{table} of the other user must survive"
);
}
// The keypair stays for queued deliveries.
for table in RETAINED_USER_TABLES {
assert_eq!(count_for(&pool, table, "user_id", &s.user).await, 1);
}
@@ -458,8 +447,6 @@ async fn the_purge_table_lists_cover_every_user_referencing_column_in_the_schema
"{t} has a foreign key to users but is not in the purge lists"
);
}
// Columns the purge matches by URL or payload must still exist, so a
// rename fails here instead of silently leaving rows behind.
for (table, column) in URL_KEYED_USER_COLUMNS
.iter()
.chain(PAYLOAD_KEYED_USER_COLUMNS)
@@ -474,9 +461,6 @@ async fn the_purge_table_lists_cover_every_user_referencing_column_in_the_schema
}
}
/// `ap_activities` (k-ap's inbound dedup) holds only remote activity ids and a
/// timestamp: nothing in it is attributable to a local user. If it ever grows a
/// column, the purge must start clearing it.
#[tokio::test]
async fn ap_activities_holds_nothing_attributable_to_a_local_user() {
let pool = migrated_pool().await;
@@ -555,7 +539,6 @@ async fn purge_strips_the_user_row_but_keeps_the_username_and_state() {
.await
.unwrap()
);
// Images stay referenced until the objects are gone.
let mut keys = p.owned_image_keys(&s.user).await.unwrap();
keys.sort();
assert_eq!(keys, vec!["avatars/a.webp", "banners/a.webp"]);
@@ -571,8 +554,6 @@ async fn purging_twice_is_a_no_op() {
parts.user.mark_deleted(&s.user, &[]).await.unwrap();
let p = purger(&pool);
p.purge(&s.user).await.unwrap();
// New data cannot appear for a purged account, but a re-run must not
// touch anything else or fail.
let before: i64 = count(&pool, "SELECT COUNT(*) FROM reviews").await;
let stamp: String = sqlx::query_scalar("SELECT purged_at FROM users WHERE id = ?")
.bind(s.user.value().to_string())
@@ -589,7 +570,6 @@ async fn purging_twice_is_a_no_op() {
.await
.unwrap();
assert_eq!(stamp, again);
// And an id that never existed is fine too.
p.purge(&UserId::generate()).await.unwrap();
}
@@ -666,7 +646,6 @@ async fn tombstone_cleanup_removes_rows_older_than_retention_and_keeps_younger_o
)
.await;
// Younger than the cutoff: kept.
let removed = p
.purge_expired_tombstones(now - chrono::Duration::days(9))
.await
@@ -674,9 +653,6 @@ async fn tombstone_cleanup_removes_rows_older_than_retention_and_keeps_younger_o
assert_eq!(removed, 0);
assert_eq!(count_for(&pool, "users", "id", &s.user).await, 1);
// Unfinished events still name the user (the pending Delete, bob's
// pending FollowRequested, a processing row): the tombstone waits and no
// unfinished row is ever deleted.
let alice = s.user.value().to_string();
let unfinished = || async {
sqlx::query_scalar::<_, i64>(
@@ -696,7 +672,6 @@ async fn tombstone_cleanup_removes_rows_older_than_retention_and_keeps_younger_o
assert_eq!(count_for(&pool, "users", "id", &s.user).await, 1);
assert_eq!(unfinished().await, before);
// A Follow that reached the shared inbox after the purge left a row.
exec(
&pool,
"INSERT INTO ap_followers (local_user_id, remote_actor_url, status, created_at)
@@ -704,7 +679,6 @@ async fn tombstone_cleanup_removes_rows_older_than_retention_and_keeps_younger_o
&[&alice],
)
.await;
// Once they finish, the cleanup takes the tombstone and the history.
exec(&pool, "UPDATE event_queue SET status = 'done'", &[]).await;
let removed = p
.purge_expired_tombstones(now - chrono::Duration::days(7))

View File

@@ -1,7 +1,3 @@
//! Real-database counterpart of the `InMemoryStore` cascade tests in the domain
//! crate. Both iterate the same table lists (`domain::testing::*_CASCADE_TABLES`),
//! so the fake and the schema cannot drift apart unnoticed.
use std::collections::BTreeSet;
use std::str::FromStr;
@@ -28,8 +24,6 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
const REMOTE_ACTOR: &str = "https://remote.example/users/ann";
/// Private in-memory database with plain `SqliteConnectOptions`; relies on
/// sqlx's default `foreign_keys = ON`, which production also depends on.
async fn migrated_pool() -> SqlitePool {
let opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
let pool = SqlitePoolOptions::new()
@@ -202,7 +196,6 @@ async fn seed_user_with_dependents(
)
.await
.unwrap();
// No repository in this crate owns these two tables (federation crate).
sqlx::query("INSERT INTO blocked_actors (local_user_id, remote_actor_url, blocked_at) VALUES (?, ?, 'now')")
.bind(user_id.value().to_string())
.bind(REMOTE_ACTOR)

View File

@@ -46,8 +46,6 @@ async fn insert_review(pool: &SqlitePool, movie: &MovieId) {
.unwrap();
}
/// Mirrors what `application::movies::merge_duplicates` builds when the
/// canonical row does not exist.
fn canonical_from(old: &Movie, canonical_id: MovieId) -> Movie {
Movie::from_persistence(
canonical_id,

View File

@@ -1,5 +1,3 @@
//! Shared helpers for the transactional-outbox repository tests.
use std::str::FromStr;
use sqlx::{
@@ -25,8 +23,6 @@ pub async fn count(pool: &SqlitePool, table: &str) -> i64 {
.unwrap()
}
/// Makes every later `event_queue` insert abort, so a write whose own row
/// already landed inside the transaction must be rolled back with it.
pub async fn fail_event_inserts(pool: &SqlitePool) {
sqlx::query(
"CREATE TRIGGER fail_event_insert BEFORE INSERT ON event_queue \

View File

@@ -1,5 +1,3 @@
//! Goal, watchlist and profile writes commit with their events or not at all.
use domain::{
events::DomainEvent,
models::{ProfileField, UserProfile, WatchlistEntry},
@@ -796,8 +794,6 @@ async fn a_failing_follow_write_rolls_back_the_rows_already_applied() {
use domain::ports::{FollowCommand, FollowWrite};
let (pool, user) = pool_with_user().await;
let mut writes = follow_writes(user.id());
// An unknown local user violates the ap_following foreign key after the
// follower row was already written.
writes.push(FollowWrite::AddFollow {
follower_id: uuid::Uuid::new_v4(),
target_actor_url: "https://md.example/users/other".into(),

View File

@@ -120,7 +120,6 @@ async fn marking_a_user_deleted_hides_them_from_every_lookup() {
repo.save(&admin).await.unwrap();
let other = test_user("other");
repo.save(&other).await.unwrap();
// A second admin, so the first is not the last one standing.
sqlx::query("UPDATE users SET role = 'admin' WHERE id = ?")
.bind(other.id().value().to_string())
.execute(&pool)
@@ -145,7 +144,6 @@ async fn marking_a_user_deleted_hides_them_from_every_lookup() {
assert!(emails.contains(&other.email().value()));
assert!(!emails.contains(&admin.email().value()));
assert_eq!(repo.count_active_admins().await.unwrap(), 1);
// The purge can still read the tombstone; a live user is not one.
assert!(
repo.find_tombstoned_by_id(admin.id())
.await