Creating a mega test binary
Overall `just test` runs about 10-20s faster on this branch than current main, for me, with around 170s instead of around 180-190s. But the cool thing is the compile time. Making a simple edit to any file involved and running `just test` on main takes about 60s to recompile the test binaries. On this branch it takes less than 15s. This is done by replacing a ton of separate test binaries with a single megabinary. Only test files that looked safe have been included, avoiding ones that do stuff like setting env.
94 unmodified lines95969798999910010110110210310410410510610710710810911011011111211311311411511611611711811911912012112212212312412512512612712812812913013113213313413413513613713813959 unmodified lines94 unmodified lines RUSTDOCFLAGS="-Zsanitizer=address" \ cargo +nightly nextest run -p tranquil-store --features tranquil-store/test-harness --target x86_64-unknown-linux-gnu
test-unit: SQLX_OFFLINE=true cargo test --test dpop_unit --test validation_edge_cases --test scope_edge_cases SQLX_OFFLINE=true cargo test --test it -- dpop_unit:: validation_edge_cases:: scope_edge_cases::
store_test := "SQLX_OFFLINE=true TRANQUIL_TEST_BACKEND=store TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 DISABLE_RATE_LIMITING=1 TRANQUIL_LEXICON_OFFLINE=1 SKIP_IMPORT_VERIFICATION=true cargo nextest run --workspace --exclude tranquil-store --features tranquil-store/skip-fsync -E 'not binary(store_parity) and not (package(tranquil-signal) and test(/^tests::/))'"store_run := "SQLX_OFFLINE=true TRANQUIL_TEST_BACKEND=store TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 DISABLE_RATE_LIMITING=1 TRANQUIL_LEXICON_OFFLINE=1 SKIP_IMPORT_VERIFICATION=true cargo nextest run --workspace --exclude tranquil-store --features tranquil-store/skip-fsync"store_test := store_run + " -E 'not (binary(it) and test(/^store_parity::/)) and not (package(tranquil-signal) and test(/^tests::/))'"
test-auth: {{store_test}} --test oauth --test oauth_lifecycle --test oauth_scopes --test oauth_security --test jwt_security --test session_management --test change_password --test password_reset {{store_run}} --test it -E 'test(/^(oauth|oauth_lifecycle|oauth_scopes|oauth_security|jwt_security|session_management|change_password|password_reset)::/)'
test-admin: {{store_test}} --test admin_email --test admin_invite --test admin_moderation --test admin_search --test admin_stats {{store_run}} --test it -E 'test(/^(admin_email|admin_invite|admin_moderation|admin_search|admin_stats)::/)'
test-sync: {{store_test}} --test sync_repo --test sync_blob --test sync_conformance --test sync_deprecated --test firehose_validation {{store_run}} --test it -E 'test(/^(sync_repo|sync_blob|sync_conformance|sync_deprecated|firehose_validation)::/)'
test-repo: {{store_test}} --test repo_batch --test repo_blob --test record_validation --test lifecycle_record {{store_run}} --test it -E 'test(/^(repo_batch|repo_blob|record_validation|lifecycle_record)::/)'
test-identity: {{store_test}} --test identity --test did_web --test plc_migration --test plc_operations --test plc_validation {{store_run}} --test it --test plc_migration -E 'binary(plc_migration) | test(/^(identity|did_web|plc_operations|plc_validation)::/)'
test-account: {{store_test}} --test lifecycle_session --test delete_account --test invite --test email_update --test account_notifications {{store_run}} --test it -E 'test(/^(lifecycle_session|delete_account|invite|email_update|account_notifications)::/)'
test-security: {{store_test}} --test security_fixes --test banned_words --test rate_limit --test moderation {{store_run}} --test it -E 'test(/^(security_fixes|banned_words|rate_limit|moderation)::/)'
test-import: {{store_test}} --test import_verification --test import_with_verification {{store_run}} --test it --test import_with_verification -E 'binary(import_with_verification) | test(/^import_verification::/)'
test-misc: {{store_test}} --test actor --test commit_signing --test image_processing --test lifecycle_social --test notifications --test server --test signing_key --test verify_live_commit {{store_run}} --test it -E 'test(/^(actor|commit_signing|image_processing|lifecycle_social|notifications|server|signing_key|verify_live_commit)::/)'
test *args: {{store_test}} {{args}}
test-one name: {{store_test}} --test {{name}} {{store_run}} --test it -E 'test(/^{{name}}::/)'
test-full *args: @just services-up eval "$(tranquil-dev-services env)" && SQLX_OFFLINE=true cargo nextest run --workspace --exclude tranquil-store --features tranquil-pds/s3 {{args}}59 unmodified lines51 unmodified lines5253545556565758596061626364646566676869707172727374757613 unmodified lines909192939494959697985 unmodified lines1041051061071081081091101111121131141151161161171181191201211221231241241251261271287 unmodified lines51 unmodified linesheavy-load-tests = { max-threads = 4 }io-heavy-sim = { max-threads = 2 }
[[profile.default.overrides]]filter = "binary(ripple_cluster)"filter = "binary(it) and test(/^ripple_cluster::/)"test-group = "serial-env-tests"
[[profile.default.overrides]]filter = "package(tranquil-signal) and test(/^tests::/)"test-group = "serial-env-tests"
[[profile.default.overrides]]filter = "binary(whole_story)"filter = "binary(it) and test(/^whole_story::/)"test-group = "heavy-load-tests"
[[profile.default.overrides]]filter = "test(/two_node_stress_concurrent_load/)"test-group = "heavy-load-tests"
[[profile.default.overrides]]filter = "binary(repo_lifecycle)"filter = "binary(it) and test(/^repo_lifecycle::/)"test-group = "heavy-load-tests"
[[profile.default.overrides]]filter = "binary(sim_soak)"13 unmodified linesfilter = "binary(gauntlet_smoke)"slow-timeout = { period = "300s", terminate-after = 8 }
[[profile.default.overrides]]filter = "binary(compaction_restart) | binary(mst_refcount_integrity) | binary(gc_compaction_restart)"filter = "binary(compaction_restart) | binary(mst_refcount_integrity) | binary(it) and test(/^gc_compaction_restart::/)"slow-timeout = { period = "120s", terminate-after = 4 }
[[profile.default.overrides]]filter = "test(/retention_time_travel_survives_many_seeds/)"5 unmodified linesslow-timeout = { period = "300s", terminate-after = 4 }test-group = "io-heavy-sim"
[[profile.ci.overrides]]filter = "binary(ripple_cluster)"filter = "binary(it) and test(/^ripple_cluster::/)"test-group = "serial-env-tests"
[[profile.ci.overrides]]filter = "package(tranquil-signal) and test(/^tests::/)"test-group = "serial-env-tests"
[[profile.ci.overrides]]filter = "binary(whole_story)"filter = "binary(it) and test(/^whole_story::/)"test-group = "heavy-load-tests"
[[profile.ci.overrides]]filter = "test(/two_node_stress_concurrent_load/)"test-group = "heavy-load-tests"
[[profile.ci.overrides]]filter = "binary(repo_lifecycle)"filter = "binary(it) and test(/^repo_lifecycle::/)"test-group = "heavy-load-tests"
[[profile.ci.overrides]]filter = "test(/retention_time_travel_survives_many_seeds/)"7 unmodified lines123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357mod common;mod helpers;use common::*;use reqwest::StatusCode;use serde_json::{Value, json};
#[tokio::test]async fn test_check_account_status_returns_correct_block_count() { let client = client(); let base = base_url().await; let (access_jwt, did) = create_account_and_login(&client).await;
let status1 = client .get(format!( "{}/xrpc/com.atproto.server.checkAccountStatus", base )) .bearer_auth(&access_jwt) .send() .await .unwrap(); assert_eq!(status1.status(), StatusCode::OK); let body1: Value = status1.json().await.unwrap(); let initial_blocks = body1["repoBlocks"].as_i64().unwrap(); assert!( initial_blocks >= 2, "New account should have at least 2 blocks (commit + empty MST)" );
let create_res = client .post(format!("{}/xrpc/com.atproto.repo.createRecord", base)) .bearer_auth(&access_jwt) .json(&json!({ "repo": did, "collection": "app.bsky.feed.post", "record": { "$type": "app.bsky.feed.post", "text": "Test post for block counting", "createdAt": chrono::Utc::now().to_rfc3339() } })) .send() .await .unwrap(); assert_eq!(create_res.status(), StatusCode::OK); let create_body: Value = create_res.json().await.unwrap(); let rkey = create_body["uri"] .as_str() .unwrap() .split('/') .next_back() .unwrap() .to_string();
let status2 = client .get(format!( "{}/xrpc/com.atproto.server.checkAccountStatus", base )) .bearer_auth(&access_jwt) .send() .await .unwrap(); let body2: Value = status2.json().await.unwrap(); let after_create_blocks = body2["repoBlocks"].as_i64().unwrap(); assert!( after_create_blocks > initial_blocks, "Block count should increase after creating a record" );
let delete_res = client .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base)) .bearer_auth(&access_jwt) .json(&json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey })) .send() .await .unwrap(); assert_eq!(delete_res.status(), StatusCode::OK);
let status3 = client .get(format!( "{}/xrpc/com.atproto.server.checkAccountStatus", base )) .bearer_auth(&access_jwt) .send() .await .unwrap(); let body3: Value = status3.json().await.unwrap(); let after_delete_blocks = body3["repoBlocks"].as_i64().unwrap(); assert!( after_delete_blocks <= after_create_blocks, "Block count should decrease or stay same after deleting a record (was {}, now {})", after_create_blocks, after_delete_blocks ); assert!( after_delete_blocks >= 2, "Block count after delete should have at least commit + MST root (got {})", after_delete_blocks );}
#[tokio::test]async fn test_check_account_status_returns_valid_repo_rev() { let client = client(); let base = base_url().await; let (access_jwt, _) = create_account_and_login(&client).await;
let status = client .get(format!( "{}/xrpc/com.atproto.server.checkAccountStatus", base )) .bearer_auth(&access_jwt) .send() .await .unwrap(); assert_eq!(status.status(), StatusCode::OK); let body: Value = status.json().await.unwrap();
let repo_rev = body["repoRev"].as_str().unwrap(); assert!(!repo_rev.is_empty(), "repoRev should not be empty"); assert!( repo_rev.chars().all(|c| c.is_alphanumeric()), "repoRev should be alphanumeric TID" );}
#[tokio::test]async fn test_check_account_status_valid_did_is_true_for_active_account() { let client = client(); let base = base_url().await; let (access_jwt, _) = create_account_and_login(&client).await;
let status = client .get(format!( "{}/xrpc/com.atproto.server.checkAccountStatus", base )) .bearer_auth(&access_jwt) .send() .await .unwrap(); assert_eq!(status.status(), StatusCode::OK); let body: Value = status.json().await.unwrap();
assert_eq!( body["validDid"], true, "validDid should be true for active account with correct DID document" ); assert_eq!( body["activated"], true, "activated should be true for active account" );}
#[tokio::test]async fn test_deactivate_account_with_delete_after() { let client = client(); let base = base_url().await; let (access_jwt, _) = create_account_and_login(&client).await;
let future_time = chrono::Utc::now() + chrono::Duration::hours(24); let delete_after = future_time.to_rfc3339();
let deactivate = client .post(format!( "{}/xrpc/com.atproto.server.deactivateAccount", base )) .bearer_auth(&access_jwt) .json(&json!({ "deleteAfter": delete_after })) .send() .await .unwrap(); assert_eq!(deactivate.status(), StatusCode::OK);
let status = client .get(format!( "{}/xrpc/com.atproto.server.checkAccountStatus", base )) .bearer_auth(&access_jwt) .send() .await .unwrap(); assert_eq!(status.status(), StatusCode::OK); let body: Value = status.json().await.unwrap(); assert_eq!(body["activated"], false, "Account should be deactivated");}
#[tokio::test]async fn test_create_account_returns_did_doc() { let client = client(); let base = base_url().await;
let handle = format!("dd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
let create_res = client .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) .json(&payload) .send() .await .unwrap(); assert_eq!(create_res.status(), StatusCode::OK); let body: Value = create_res.json().await.unwrap();
assert!( body["accessJwt"].is_string(), "accessJwt should always be returned" ); assert!( body["refreshJwt"].is_string(), "refreshJwt should always be returned" ); assert!(body["did"].is_string(), "did should be returned");
if body["didDoc"].is_object() { let did_doc = &body["didDoc"]; assert!(did_doc["id"].is_string(), "didDoc should have id field"); }}
#[tokio::test]async fn test_create_account_always_returns_tokens() { let client = client(); let base = base_url().await;
let handle = format!("tt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
let create_res = client .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) .json(&payload) .send() .await .unwrap(); assert_eq!(create_res.status(), StatusCode::OK); let body: Value = create_res.json().await.unwrap();
let access_jwt = body["accessJwt"] .as_str() .expect("accessJwt should be present"); let refresh_jwt = body["refreshJwt"] .as_str() .expect("refreshJwt should be present");
assert!(!access_jwt.is_empty(), "accessJwt should not be empty"); assert!(!refresh_jwt.is_empty(), "refreshJwt should not be empty");
let parts: Vec<&str> = access_jwt.split('.').collect(); assert_eq!( parts.len(), 3, "accessJwt should be a valid JWT with 3 parts" );}
#[tokio::test]async fn test_describe_server_has_links_and_contact() { let client = client(); let base = base_url().await;
let describe = client .get(format!("{}/xrpc/com.atproto.server.describeServer", base)) .send() .await .unwrap(); assert_eq!(describe.status(), StatusCode::OK); let body: Value = describe.json().await.unwrap();
assert!( body.get("links").is_some(), "describeServer should include links object" ); assert!( body.get("contact").is_some(), "describeServer should include contact object" );
let links = &body["links"]; assert!( links.get("privacyPolicy").is_some() || links["privacyPolicy"].is_null(), "links should have privacyPolicy field (can be null)" ); assert!( links.get("termsOfService").is_some() || links["termsOfService"].is_null(), "links should have termsOfService field (can be null)" );
let contact = &body["contact"]; assert!( contact.get("email").is_some() || contact["email"].is_null(), "contact should have email field (can be null)" );}
#[tokio::test]async fn test_delete_account_password_max_length() { let client = client(); let base = base_url().await;
let handle = format!("pl{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
let create_res = client .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) .json(&payload) .send() .await .unwrap(); assert_eq!(create_res.status(), StatusCode::OK); let body: Value = create_res.json().await.unwrap(); let did = body["did"].as_str().unwrap();
let too_long_password = "a".repeat(600); let delete_res = client .post(format!("{}/xrpc/com.atproto.server.deleteAccount", base)) .json(&json!({ "did": did, "password": too_long_password, "token": "fake-token" })) .send() .await .unwrap();
assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST); let error_body: Value = delete_res.json().await.unwrap(); assert!( error_body["message"] .as_str() .unwrap() .contains("password length") || error_body["error"].as_str().unwrap() == "InvalidRequest" );}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300mod common;use common::{base_url, client, create_account_and_login, get_test_repos, user_id_of};use serde_json::{Value, json};use tranquil_db_traits::{CommsChannel, CommsType};use tranquil_types::{Did, Recipient};
type Repos = tranquil_db::PostgresRepositories;
async fn set_prefs( client: &reqwest::Client, base: &str, token: &str, prefs: serde_json::Value,) -> reqwest::Response { client .post(format!("{}/xrpc/_account.updateNotificationPrefs", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap()}
async fn confirm_channel( client: &reqwest::Client, base: &str, token: &str, channel: &str, id: &str, code: &str,) -> reqwest::Response { client .post(format!("{}/xrpc/_account.confirmChannelVerification", base)) .header("Authorization", format!("Bearer {}", token)) .json(&json!({"channel": channel, "identifier": id, "code": code})) .send() .await .unwrap()}
async fn latest_notices( repos: &Repos, user_id: uuid::Uuid, n: i64,) -> Vec<tranquil_db_traits::QueuedComms> { repos .infra .get_latest_comms_for_user(user_id, CommsType::ChannelVerified, n) .await .expect("DB error")}
#[tokio::test]async fn test_get_notification_history() { let client = client(); let base = base_url().await; let repos = get_test_repos().await; let (token, did) = create_account_and_login(&client).await;
let user_id = user_id_of(repos, &Did::new(did).unwrap()).await;
for i in 0..3 { repos .infra .enqueue_comms( Some(user_id), &Recipient::new(CommsChannel::Email, "test@nel.pet").unwrap(), CommsType::Welcome, Some(&format!("Subject {}", i)), &format!("Body {}", i), None, ) .await .expect("Failed to enqueue"); }
let resp = client .get(format!("{}/xrpc/_account.getNotificationHistory", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap();
assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); let notifications = body["notifications"].as_array().unwrap(); assert_eq!(notifications.len(), 5);
assert_eq!(notifications[0]["subject"], "Subject 2"); assert_eq!(notifications[1]["subject"], "Subject 1"); assert_eq!(notifications[2]["subject"], "Subject 0");}
#[tokio::test]async fn test_verify_channel_discord() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await;
let resp = set_prefs( &client, base, &token, json!({ "discordUsername": "testuser123" }), ) .await; assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); assert!( body["verificationRequired"] .as_array() .unwrap() .contains(&json!("discord")) );
let resp = client .get(format!("{}/xrpc/_account.getNotificationPrefs", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); let body: Value = resp.json().await.unwrap(); assert_eq!(body["discordVerified"], false); assert_eq!(body["discordUsername"], "testuser123");}
#[tokio::test]async fn test_verify_channel_invalid_code() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await;
let resp = set_prefs( &client, base, &token, json!({ "telegramUsername": "testuser" }), ) .await; assert_eq!(resp.status(), 200);
let resp = confirm_channel( &client, base, &token, "telegram", "testuser", "XXXX-XXXX-XXXX-XXXX", ) .await; assert_eq!(resp.status(), 400);}
#[tokio::test]async fn test_channel_verified_notice_delivers_over_email_until_chat_id_is_stored() { let client = client(); let base = base_url().await; let repos = get_test_repos().await; let (token, did) = create_account_and_login(&client).await; let did = Did::new(did).unwrap(); let user_id = user_id_of(repos, &did).await;
let id = "10987654321"; let resp = set_prefs(&client, base, &token, json!({ "telegramUsername": id })).await; assert_eq!(resp.status(), 200);
let code = |did: &Did| { tranquil_pds::auth::verification_token::generate_channel_update_token( did, CommsChannel::Telegram, id, ) }; let resp = confirm_channel(&client, base, &token, "telegram", id, &code(&did)).await; assert_eq!(resp.status(), 200);
let snapshot = |notices: &[tranquil_db_traits::QueuedComms]| { notices .iter() .map(|notice| (notice.channel, notice.recipient.clone())) .collect::<Vec<_>>() }; let notices = latest_notices(repos, user_id, 5).await; assert!( notices .iter() .all(|notice| notice.channel != CommsChannel::Telegram), "Telegram identifier entered the queue as a chat ID: {:?}", snapshot(¬ices) ); assert!( notices .iter() .any(|notice| notice.channel == CommsChannel::Email), "The notice should fall back to email: {:?}", snapshot(¬ices) );
repos .user .store_telegram_chat_id( &tranquil_types::TelegramUsername::new(id).unwrap(), 10987654321, None, ) .await .expect("DB error") .expect("The Telegram username didn't match a user");
let resp = confirm_channel(&client, base, &token, "telegram", id, &code(&did)).await; assert_eq!(resp.status(), 200);
let notices = latest_notices(repos, user_id, 10).await; assert!( notices .iter() .any(|notice| notice.channel == CommsChannel::Telegram && notice.recipient == "10987654321"), "A stored chat ID should receive the notice: {:?}", snapshot(¬ices) );}
#[tokio::test]async fn test_verify_channel_not_set() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await;
let resp = confirm_channel( &client, base, &token, "signal", "123456", "XXXX-XXXX-XXXX-XXXX", ) .await; assert_eq!(resp.status(), 400);}
#[tokio::test]async fn test_update_email_via_notification_prefs() { let client = client(); let base = base_url().await; let repos = get_test_repos().await; let (token, did) = create_account_and_login(&client).await;
let unique_email = format!("newemail_{}@jola.dev", uuid::Uuid::new_v4()); let resp = set_prefs(&client, base, &token, json!({ "email": unique_email })).await; assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); assert!( body["verificationRequired"] .as_array() .unwrap() .contains(&json!("email")) );
let user_id = user_id_of(repos, &Did::new(did).unwrap()).await;
let comms = repos .infra .get_latest_comms_for_user(user_id, CommsType::EmailUpdate, 1) .await .expect("DB error"); let body_text = comms .first() .map(|c| c.body.clone()) .expect("Verification code not found");
let code = body_text .lines() .skip_while(|line| !line.contains("verification code")) .nth(1) .map(|line| line.trim().to_string()) .filter(|line| !line.is_empty()) .unwrap_or_else(|| { body_text .lines() .find(|line| { let trimmed = line.trim(); trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-') }) .map(|s| s.trim().to_string()) .unwrap_or_default() });
let resp = confirm_channel(&client, base, &token, "email", &unique_email, &code).await; assert_eq!(resp.status(), 200);
let resp = client .get(format!("{}/xrpc/_account.getNotificationPrefs", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); let body: Value = resp.json().await.unwrap(); assert_eq!(body["email"], unique_email);}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605mod common;use common::{base_url, client, create_account_and_login};use serde_json::{Value, json};
#[tokio::test]async fn test_get_preferences_empty() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); assert!(body.get("preferences").is_some()); assert!(body["preferences"].as_array().unwrap().is_empty());}
#[tokio::test]async fn test_get_preferences_no_auth() { let client = client(); let base = base_url().await; let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .send() .await .unwrap(); assert_eq!(resp.status(), 401);}
#[tokio::test]async fn test_put_preferences_success() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": true }, { "$type": "app.bsky.actor.defs#contentLabelPref", "label": "nsfw", "visibility": "warn" } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 2); let adult_pref = prefs_arr.iter().find(|p| { p.get("$type").and_then(|t| t.as_str()) == Some("app.bsky.actor.defs#adultContentPref") }); assert!(adult_pref.is_some()); assert_eq!(adult_pref.unwrap()["enabled"], true);}
#[tokio::test]async fn test_put_preferences_multiple_same_type() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": false }, { "$type": "app.bsky.actor.defs#contentLabelPref", "label": "dogs", "visibility": "show" }, { "$type": "app.bsky.actor.defs#contentLabelPref", "label": "cats", "visibility": "warn" } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 3); let adult_pref = prefs_arr.iter().find(|p| { p.get("$type").and_then(|t| t.as_str()) == Some("app.bsky.actor.defs#adultContentPref") }); assert!(adult_pref.is_some()); assert_eq!(adult_pref.unwrap()["enabled"], false); let content_label_prefs: Vec<&Value> = prefs_arr .iter() .filter(|p| { p.get("$type").and_then(|t| t.as_str()) == Some("app.bsky.actor.defs#contentLabelPref") }) .collect(); assert_eq!(content_label_prefs.len(), 2); let dogs_pref = content_label_prefs .iter() .find(|p| p.get("label").and_then(|l| l.as_str()) == Some("dogs")); assert!(dogs_pref.is_some()); assert_eq!(dogs_pref.unwrap()["visibility"], "show"); let cats_pref = content_label_prefs .iter() .find(|p| p.get("label").and_then(|l| l.as_str()) == Some("cats")); assert!(cats_pref.is_some()); assert_eq!(cats_pref.unwrap()["visibility"], "warn");}
#[tokio::test]async fn test_put_preferences_no_auth() { let client = client(); let base = base_url().await; let prefs = json!({ "preferences": [] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 401);}
#[tokio::test]async fn test_put_preferences_missing_type() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "enabled": true } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 400); let body: Value = resp.json().await.unwrap(); assert_eq!(body["error"], "InvalidRequest");}
#[tokio::test]async fn test_put_preferences_invalid_namespace() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "com.example.somePref", "value": "test" } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 400); let body: Value = resp.json().await.unwrap(); assert_eq!(body["error"], "InvalidRequest");}
#[tokio::test]async fn test_put_preferences_read_only_silently_filtered() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#declaredAgePref", "isOverAge18": true }, { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": true } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let get_resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(get_resp.status(), 200); let body: Value = get_resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 1); assert_eq!( prefs_arr[0]["$type"], "app.bsky.actor.defs#adultContentPref" );}
#[tokio::test]async fn test_put_preferences_replaces_all() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs1 = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": true }, { "$type": "app.bsky.actor.defs#contentLabelPref", "label": "nsfw", "visibility": "warn" } ] }); client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs1) .send() .await .unwrap(); let prefs2 = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#threadViewPref", "sort": "newest" } ] }); client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs2) .send() .await .unwrap(); let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 1); assert_eq!(prefs_arr[0]["$type"], "app.bsky.actor.defs#threadViewPref");}
#[tokio::test]async fn test_put_preferences_saved_feeds() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#savedFeedsPrefV2", "items": [ { "type": "feed", "value": "at://did:plc:example/app.bsky.feed.generator/my-feed", "pinned": true } ] } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 1); let saved_feeds = &prefs_arr[0]; assert_eq!(saved_feeds["$type"], "app.bsky.actor.defs#savedFeedsPrefV2"); assert!(saved_feeds["items"].as_array().unwrap().len() == 1);}
#[tokio::test]async fn test_put_preferences_muted_words() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#mutedWordsPref", "items": [ { "value": "spoiler", "targets": ["content", "tag"], "actorTarget": "all" } ] } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); let body: Value = resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr[0]["$type"], "app.bsky.actor.defs#mutedWordsPref");}
#[tokio::test]async fn test_preferences_isolation_between_users() { let client = client(); let base = base_url().await; let (token1, _did1) = create_account_and_login(&client).await; let (token2, _did2) = create_account_and_login(&client).await; let prefs1 = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": true } ] }); client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token1)) .json(&prefs1) .send() .await .unwrap(); let resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token2)) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body: Value = resp.json().await.unwrap(); assert!(body["preferences"].as_array().unwrap().is_empty());}
#[tokio::test]async fn test_declared_age_pref_computed_from_birth_date() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#personalDetailsPref", "birthDate": "1990-01-15" } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let get_resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(get_resp.status(), 200); let body: Value = get_resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 2); let personal_details = prefs_arr .iter() .find(|p| p["$type"] == "app.bsky.actor.defs#personalDetailsPref"); assert!(personal_details.is_some()); assert_eq!(personal_details.unwrap()["birthDate"], "1990-01-15"); let declared_age = prefs_arr .iter() .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); assert!(declared_age.is_some()); let declared_age = declared_age.unwrap(); assert_eq!(declared_age["isOverAge13"], true); assert_eq!(declared_age["isOverAge16"], true); assert_eq!(declared_age["isOverAge18"], true);}
#[tokio::test]async fn test_declared_age_pref_computed_under_18() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; let current_year = chrono::Utc::now() .format("%Y") .to_string() .parse::<i32>() .unwrap(); let birth_year = current_year - 15; let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#personalDetailsPref", "birthDate": format!("{}-06-15", birth_year) } ] }); let resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let get_resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(get_resp.status(), 200); let body: Value = get_resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); let declared_age = prefs_arr .iter() .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); assert!(declared_age.is_some()); let declared_age = declared_age.unwrap(); assert_eq!(declared_age["isOverAge13"], true); assert_eq!(declared_age["isOverAge16"], false); assert_eq!(declared_age["isOverAge18"], false);}
#[tokio::test]async fn test_deactivated_account_can_get_preferences() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await;
let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": true } ] }); let put_resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!(put_resp.status(), 200);
let deactivate = client .post(format!( "{}/xrpc/com.atproto.server.deactivateAccount", base )) .header("Authorization", format!("Bearer {}", token)) .json(&json!({})) .send() .await .unwrap(); assert_eq!(deactivate.status(), 200);
let get_resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!( get_resp.status(), 200, "Deactivated account should still be able to get preferences" ); let body: Value = get_resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 1);}
#[tokio::test]async fn test_deactivated_account_can_put_preferences() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await;
let deactivate = client .post(format!( "{}/xrpc/com.atproto.server.deactivateAccount", base )) .header("Authorization", format!("Bearer {}", token)) .json(&json!({})) .send() .await .unwrap(); assert_eq!(deactivate.status(), 200);
let prefs = json!({ "preferences": [ { "$type": "app.bsky.actor.defs#adultContentPref", "enabled": true } ] }); let put_resp = client .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .json(&prefs) .send() .await .unwrap(); assert_eq!( put_resp.status(), 200, "Deactivated account should still be able to put preferences" );
let get_resp = client .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(get_resp.status(), 200); let body: Value = get_resp.json().await.unwrap(); let prefs_arr = body["preferences"].as_array().unwrap(); assert_eq!(prefs_arr.len(), 1);}