purge: skip broadcasts of deleted authors, purge outbox log, guards, 410 inbox test

This commit is contained in:
2026-10-26 02:39:06 +01:00
parent 51a70b5260
commit 80dbdec107
9 changed files with 318 additions and 27 deletions

View File

@@ -14,9 +14,9 @@ use domain::ports::{
use domain::testing::{test_goal, test_movie, test_review, test_user, test_watched_at};
use domain::value_objects::{FollowStatus, InstanceIdentity, UserId};
use sqlite::{
PURGED_USER_TABLES, RETAINED_USER_TABLES, SqliteAccountPurger, SqliteProfileFieldsRepository,
SqliteSocialRepository, SqliteWatchEventRepository, SqliteWebhookTokenRepository,
URL_KEYED_USER_TABLES,
PAYLOAD_KEYED_USER_COLUMNS, PURGED_USER_TABLES, RETAINED_USER_TABLES, SqliteAccountPurger,
SqliteProfileFieldsRepository, SqliteSocialRepository, SqliteWatchEventRepository,
SqliteWebhookTokenRepository, URL_KEYED_USER_COLUMNS,
};
use sqlx::SqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
@@ -266,6 +266,45 @@ async fn seed(pool: &SqlitePool, parts: &sqlite::SqliteWireOutput) -> Seeded {
)
.await;
}
for (event_type, status, payload) in [
(
"ReviewLogged",
"done",
format!(r#"{{"user_id":"{a_str}","comment":"secret"}}"#),
),
(
"FederationDeliveryRequested",
"done",
format!(
r#"{{"signing_actor_id":"{a_str}","activity_json":{{"type":"Create","content":"secret"}}}}"#
),
),
(
"FederationDeliveryRequested",
"pending",
format!(
r#"{{"signing_actor_id":"{a_str}","activity_json":{{"type":"Delete","id":"x"}}}}"#
),
),
(
"UserDeleted",
"processing",
format!(r#"{{"user_id":"{a_str}"}}"#),
),
(
"ReviewLogged",
"done",
format!(r#"{{"user_id":"{b_str}"}}"#),
),
] {
sqlx::query("INSERT INTO event_queue (event_type, payload, status) VALUES (?, ?, ?)")
.bind(event_type)
.bind(payload)
.bind(status)
.execute(pool)
.await
.unwrap();
}
Seeded {
user: a,
other: b,
@@ -332,6 +371,24 @@ async fn purge_removes_every_user_owned_row() {
.unwrap();
assert_eq!(announces, vec!["an3"], "only bob's announce survives");
let queue: Vec<(String, String)> =
sqlx::query_as("SELECT event_type, status FROM event_queue ORDER BY id")
.fetch_all(&pool)
.await
.unwrap();
assert_eq!(
queue,
vec![
(
"FederationDeliveryRequested".to_string(),
"pending".to_string()
),
("UserDeleted".to_string(), "processing".to_string()),
("ReviewLogged".to_string(), "done".to_string()),
],
"the user's authored events and activities are gone; the pending Delete, the trigger and bob's event stay"
);
// The other account is untouched.
for (table, column) in PURGED_USER_TABLES {
assert!(
@@ -384,17 +441,36 @@ 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"
);
}
// URL-keyed tables must exist, so a rename cannot silently drop them.
for t in URL_KEYED_USER_TABLES {
let n: i64 = count(
&pool,
&format!("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='{t}'"),
)
.await;
assert_eq!(n, 1, "{t} is listed as URL-keyed but missing");
// 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)
{
let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?")
.bind(table)
.bind(column)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(n, 1, "{table}.{column} is matched by the purge but missing");
}
}
/// `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;
let columns: Vec<String> =
sqlx::query_scalar("SELECT name FROM pragma_table_info('ap_activities') ORDER BY name")
.fetch_all(&pool)
.await
.unwrap();
assert_eq!(columns, vec!["id", "processed_at"]);
}
#[tokio::test]
async fn purge_keeps_movies_and_metadata() {
let pool = migrated_pool().await;