Files
maarcadetweet/crates/pds-server/tests/pds_integration.rs
T
tomdebone c586fd39c9 maarcadetweet: initial commit
AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
2026-07-05 20:01:31 +02:00

1715 lines
54 KiB
Rust

use serde_json::{json, Value};
use std::time::Duration;
const PDS_URL: &str = "http://127.0.0.1:2583";
async fn client() -> reqwest::Client {
reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap()
}
async fn wait_for_pds() -> bool {
let c = client().await;
for _ in 0..20 {
if let Ok(r) = c.get(format!("{}/healthz", PDS_URL)).send().await {
if r.status().is_success() {
return true;
}
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
false
}
#[tokio::test]
async fn describe_server() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let r: Value = c
.get(format!("{}/xrpc/com.atproto.server.describeServer", PDS_URL))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(r["did"].is_string());
assert!(r["available_user_domains"].is_array());
assert_eq!(r["invite_code_required"], json!(false));
}
#[tokio::test]
async fn create_account_session_refresh_resolve() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let handle = format!("itest_{}.maarcadetweet.local", uuid::Uuid::new_v4().simple());
let pw = "hunter2hunter2";
let r: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({
"handle": handle,
"email": "i@test.com",
"password": pw,
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(r["did"].is_string(), "createAccount: {:?}", r);
assert_eq!(r["handle"], json!(handle));
assert!(r["access_jwt"].is_string());
let did = r["did"].as_str().unwrap().to_string();
let access = r["access_jwt"].as_str().unwrap().to_string();
let refresh = r["refresh_jwt"].as_str().unwrap().to_string();
let r2: Value = c
.post(format!("{}/xrpc/com.atproto.server.createSession", PDS_URL))
.json(&json!({"identifier": handle, "password": pw}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(r2["did"], json!(did));
assert!(r2["access_jwt"].is_string());
let r3: Value = c
.post(format!("{}/xrpc/com.atproto.server.refreshSession", PDS_URL))
.json(&json!({"refresh_jwt": refresh}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(r3["access_jwt"].is_string());
assert_eq!(r3["did"], json!(did));
let r4: Value = c
.post(format!("{}/xrpc/com.atproto.identity.resolveHandle", PDS_URL))
.json(&json!({"handle": handle}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(r4["did"], json!(did));
let _ = access;
}
#[tokio::test]
async fn create_record_enforces_160_chars() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let handle = format!("rec_{}.maarcadetweet.local", uuid::Uuid::new_v4().simple());
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
let ok_resp = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {"text": "ok 140 chars total, fits.", "createdAt": "2026-07-01T12:00:00Z"},
}))
.send()
.await
.unwrap();
let ok_body: Value = ok_resp.json().await.unwrap();
assert!(ok_body["uri"].is_string(), "expected uri, got: {:?}", ok_body);
let long = "x".repeat(161);
let r401 = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {"text": long, "createdAt": "2026-07-01T12:00:00Z"},
}))
.send()
.await
.unwrap();
assert_eq!(r401.status().as_u16(), 400);
let body: Value = r401.json().await.unwrap();
assert_eq!(body["error"], json!("InvalidRequest"));
assert!(body["message"]
.as_str()
.unwrap()
.contains("max length"));
let r_400_empty = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {"text": "", "createdAt": "2026-07-01T12:00:00Z"},
}))
.send()
.await
.unwrap();
assert_eq!(r_400_empty.status().as_u16(), 400);
let r_400_missing = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {"text": "x"},
}))
.send()
.await
.unwrap();
assert_eq!(r_400_missing.status().as_u16(), 400);
let r_unknown = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.unknown.thing",
"record": {"text": "x", "createdAt": "2026-07-01T12:00:00Z"},
}))
.send()
.await
.unwrap();
assert_eq!(r_unknown.status().as_u16(), 400);
let r_noauth = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {"text": "x", "createdAt": "2026-07-01T12:00:00Z"},
}))
.send()
.await
.unwrap();
assert_eq!(r_noauth.status().as_u16(), 401);
}
#[tokio::test]
async fn rejects_duplicate_handle() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let handle = format!("dup_{}.maarcadetweet.local", uuid::Uuid::new_v4().simple());
let r1: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(r1["did"].is_string());
let r2 = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap();
assert_eq!(r2.status().as_u16(), 409);
}
#[tokio::test]
async fn rejects_short_password() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let r = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({
"handle": format!("sp_{}.maarcadetweet.local", uuid::Uuid::new_v4().simple()),
"password": "short"
}))
.send()
.await
.unwrap();
assert_eq!(r.status().as_u16(), 400);
}
// -- sync endpoint tests ---------------------------------------------------
/// Minimal CAR v1 parser used to inspect what the server returned. This is
/// intentionally simple — it just extracts the header, root CIDs and (cid,
/// block) pairs. The point of the tests is to verify the on-the-wire format,
/// not to re-implement a full CAR library.
#[derive(Debug, Default)]
struct ParsedCar {
version: u64,
roots: Vec<String>,
blocks: Vec<(String, Vec<u8>)>,
}
fn parse_car(bytes: &[u8]) -> ParsedCar {
let mut out = ParsedCar::default();
let mut p = 0usize;
// Read a LEB128 varint.
fn read_varint(bytes: &[u8], pos: &mut usize) -> u64 {
let mut value: u64 = 0;
let mut shift = 0u32;
loop {
let b = bytes[*pos];
*pos += 1;
value |= ((b & 0x7f) as u64) << shift;
if b & 0x80 == 0 {
return value;
}
shift += 7;
}
}
// Read a CBOR "head": single byte for value <= 23, otherwise head +
// 1/2/4/8 extra bytes for info 24/25/26/27. Returns (major, value,
// bytes_consumed).
fn read_cbor_head(bytes: &[u8], pos: usize) -> (u8, u64, usize) {
let first = bytes[pos];
let major = first >> 5;
let info = first & 0x1f;
let (value, extra) = match info {
0..=23 => (info as u64, 0usize),
24 => (bytes[pos + 1] as u64, 1),
25 => (((bytes[pos + 1] as u64) << 8) | (bytes[pos + 2] as u64), 2),
26 => (
((bytes[pos + 1] as u64) << 24)
| ((bytes[pos + 2] as u64) << 16)
| ((bytes[pos + 3] as u64) << 8)
| (bytes[pos + 4] as u64),
4,
),
27 => {
let mut n = 0u64;
for i in 0..8 {
n = (n << 8) | (bytes[pos + 1 + i] as u64);
}
(n, 8)
}
other => panic!("unsupported CBOR info {other}"),
};
(major, value, 1 + extra)
}
// Header
let header_len = read_varint(bytes, &mut p) as usize;
let _header_end = p + header_len;
let (maj, n_items, consumed) = read_cbor_head(bytes, p);
assert_eq!(maj, 5, "header must be a CBOR map");
p += consumed;
assert_eq!(n_items, 2, "header must have 2 keys");
for _ in 0..2 {
let (maj, n, c) = read_cbor_head(bytes, p);
assert_eq!(maj, 3, "key must be a text string");
p += c;
let key = std::str::from_utf8(&bytes[p..p + n as usize])
.unwrap()
.to_string();
p += n as usize;
if key == "version" {
let (maj, v, c) = read_cbor_head(bytes, p);
assert_eq!(maj, 0, "version must be an unsigned int");
p += c;
out.version = v;
} else if key == "roots" {
let (maj, n, c) = read_cbor_head(bytes, p);
assert_eq!(maj, 4, "roots must be a CBOR array");
p += c;
for _ in 0..n {
let (maj, _, c) = read_cbor_head(bytes, p);
assert_eq!(maj, 6, "root CID must be a tagged value");
p += c;
let (maj, ln, c) = read_cbor_head(bytes, p);
assert_eq!(maj, 2, "root CID must be a byte string");
p += c;
let cid_bytes = &bytes[p..p + ln as usize];
let cid_hex: String = cid_bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect();
p += ln as usize;
out.roots.push(format!("raw:{}", cid_hex));
}
} else {
panic!("unexpected header key {key}");
}
}
// Body sections
while p < bytes.len() {
let section_len = read_varint(bytes, &mut p) as usize;
let section_end = p + section_len;
// CID = varint version + varint codec + (multihash = code + size + digest)
let cid_start = p;
let _v = read_varint(bytes, &mut p);
let _c = read_varint(bytes, &mut p);
// Multihash: read code, then size, then `size` bytes of digest.
let _mh_code = read_varint(bytes, &mut p);
let mh_size = read_varint(bytes, &mut p) as usize;
let cid_end = p;
p += mh_size;
let cid_hex: String = bytes[cid_start..cid_end + mh_size]
.iter()
.map(|b| format!("{:02x}", b))
.collect();
let data = bytes[p..section_end].to_vec();
p = section_end;
out.blocks.push((cid_hex, data));
}
out
}
/// Create a fresh user and a few records. Returns the http client, the
/// user's DID, the access JWT, and the list of record value CIDs.
async fn fresh_user_with_records() -> (reqwest::Client, String, String, Vec<String>) {
let c = client().await;
let handle = format!("sync_{}.maarcadetweet.local", uuid::Uuid::new_v4().simple());
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
let mut record_cids = Vec::new();
for i in 0..3 {
let resp: Value = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": format!("hello sync #{i}"),
"createdAt": "2026-07-01T12:00:00Z",
},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(resp["uri"].is_string(), "createRecord: {:?}", resp);
record_cids.push(resp["cid"].as_str().unwrap().to_string());
}
(c, did, jwt, record_cids)
}
#[tokio::test]
async fn sync_get_repo_returns_valid_car() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, _jwt, record_cids) = fresh_user_with_records().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo?did={}",
PDS_URL, did
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or(""),
"application/vnd.ipld.car"
);
let bytes = resp.bytes().await.unwrap();
assert!(!bytes.is_empty());
let parsed = parse_car(&bytes);
assert_eq!(parsed.version, 1);
assert_eq!(parsed.roots.len(), 1);
// Every record CID we created should appear as a block in the CAR.
for cid in &record_cids {
// The CAR stores the raw CID bytes; we re-encode the multibase
// string into the same raw form to compare.
let cid_obj: cid::Cid = cid.parse().unwrap();
let raw_hex: String = cid_obj
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert!(
parsed.blocks.iter().any(|(c, _)| c == &raw_hex),
"CAR should contain record {cid} (raw: {raw_hex})"
);
}
// The CAR should also contain the MST root block (DAG-CBOR of a node)
// and the head commit block. The MST root is a `tag(42) + cid_link`
// node, and the head commit is a DAG-CBOR map with did/version/rev.
assert!(
parsed.blocks.len() >= 5,
"CAR should have at least head commit + MST root + 3 records; got {}",
parsed.blocks.len()
);
}
#[tokio::test]
async fn sync_get_repo_missing_did_returns_400() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo?did=did:plc:nope%sinvalid",
PDS_URL
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
let body: Value = resp.json().await.unwrap();
assert_eq!(body["error"], json!("RepoNotFound"));
}
#[tokio::test]
async fn sync_get_blocks_returns_requested_cids() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, _jwt, record_cids) = fresh_user_with_records().await;
// First fetch getRepo so we can pick out arbitrary blocks (e.g. MST
// nodes) in addition to record value blocks.
let repo_bytes = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo?did={}",
PDS_URL, did
))
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let _parsed = parse_car(&repo_bytes);
// Ask getBlocks for just one record CID. We pick the first record we
// created, which we know is in the repo.
let some_record = &record_cids[0];
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}",
PDS_URL, did, some_record
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or(""),
"application/vnd.ipld.car"
);
let bytes = resp.bytes().await.unwrap();
let parsed = parse_car(&bytes);
// The CAR should contain exactly the blocks we requested (modulo the
// dedup behaviour — at minimum the record block).
let target_record_hex = {
let cid_obj: cid::Cid = some_record.parse().unwrap();
cid_obj
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
};
assert!(
parsed
.blocks
.iter()
.any(|(c, _)| c == &target_record_hex),
"getBlocks should include the requested record; got blocks: {:?}",
parsed.blocks.iter().map(|(c, _)| c).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn sync_get_blocks_invalid_cid_returns_400() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getBlocks?did=did:plc:anything&cids=not-a-cid",
PDS_URL
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
let body: Value = resp.json().await.unwrap();
assert_eq!(body["error"], json!("InvalidRequest"));
}
#[tokio::test]
async fn sync_get_latest_commit_returns_json() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, _jwt, _cids) = fresh_user_with_records().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getLatestCommit?did={}",
PDS_URL, did
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let body: Value = resp.json().await.unwrap();
let cid_str = body["cid"].as_str().expect("cid should be a string");
let rev_str = body["rev"].as_str().expect("rev should be a string");
let parsed: cid::Cid = cid_str.parse().expect("cid should parse");
// The latest commit CID must be a CIDv1 DAG-CBOR block (0x71).
assert_eq!(parsed.codec(), 0x71);
assert!(!rev_str.is_empty());
// The returned CID should match the one we see as the root of getRepo.
let repo = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo?did={}",
PDS_URL, did
))
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let parsed_repo = parse_car(&repo);
let root_hex = parsed_repo.roots[0]
.trim_start_matches("raw:")
.to_string();
let expected_hex: String = parsed
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert_eq!(root_hex, expected_hex, "getLatestCommit cid must equal getRepo root");
}
#[tokio::test]
async fn sync_get_latest_commit_missing_did() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getLatestCommit?did=did:plc:no-such-did",
PDS_URL
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
let body: Value = resp.json().await.unwrap();
assert_eq!(body["error"], json!("RepoNotFound"));
}
#[tokio::test]
async fn sync_get_record_returns_value_block() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, jwt, record_cids) = fresh_user_with_records().await;
// We need an (rkey, value_cid) pair. Re-fetch the records: each
// createRecord returns a uri+cid, and the rkey is the trailing tid.
let first: Value = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {"text": "first post for sync getRecord", "createdAt": "2026-07-01T12:00:00Z"},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let uri = first["uri"].as_str().unwrap();
let value_cid = first["cid"].as_str().unwrap();
let rkey = uri.rsplit('/').next().unwrap().to_string();
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.twi.post&rkey={}",
PDS_URL, did, rkey
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or(""),
"application/vnd.ipld.car"
);
let bytes = resp.bytes().await.unwrap();
let parsed = parse_car(&bytes);
let target_hex: String = value_cid
.parse::<cid::Cid>()
.unwrap()
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert!(
parsed.blocks.iter().any(|(c, _)| c == &target_hex),
"getRecord CAR should include the value block {value_cid} (raw: {target_hex})"
);
// The record_cids list (from the helper) and our new first CID should
// both be valid CIDs; the test really only checks the value block is
// there, the other one is sanity.
assert!(!record_cids.is_empty());
}
#[tokio::test]
async fn sync_get_record_missing_returns_404() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, _jwt, _cids) = fresh_user_with_records().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.twi.post&rkey=doesnotexist",
PDS_URL, did
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 404);
let body: Value = resp.json().await.unwrap();
assert_eq!(body["error"], json!("RecordNotFound"));
}
#[tokio::test]
async fn sync_list_repos_includes_recent_user() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, _jwt, _cids) = fresh_user_with_records().await;
// Page through listRepos with a small limit until we see our DID.
let mut cursor: Option<String> = None;
let mut found = false;
for _ in 0..50 {
let url = match &cursor {
Some(c) => format!(
"{}/xrpc/com.atproto.sync.listRepos?limit=50&cursor={}",
PDS_URL, c
),
None => format!("{}/xrpc/com.atproto.sync.listRepos?limit=50", PDS_URL),
};
let resp = c.get(&url).send().await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let body: Value = resp.json().await.unwrap();
let repos = body["repos"].as_array().expect("repos array");
if repos.iter().any(|r| r["did"] == json!(did)) {
found = true;
// The matching entry should have a `head` (CID) and `rev`.
let r = repos.iter().find(|r| r["did"] == json!(did)).unwrap();
assert!(r["head"].is_string(), "head: {:?}", r);
assert!(r["rev"].is_string(), "rev: {:?}", r);
assert_eq!(r["active"], json!(true));
// head must parse as a CID
let _cid: cid::Cid = r["head"].as_str().unwrap().parse().unwrap();
break;
}
match body["cursor"].as_str() {
Some(c) => cursor = Some(c.to_string()),
None => break,
}
}
assert!(found, "listRepos should include the freshly created DID {did}");
}
// -- new Phase 2c tests ----------------------------------------------------
/// Create a single account with a deterministic handle, returning
/// (client, did, jwt).
async fn fresh_account(handle_suffix: &str) -> (reqwest::Client, String, String) {
let c = client().await;
let handle = format!(
"lp{}_{}.maarcadetweet.local",
handle_suffix,
uuid::Uuid::new_v4().simple()
);
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
(c, did, jwt)
}
/// Recompute the CID for a CAR body + raw CID bytes and compare. Returns
/// true if they match.
fn cid_matches_block(cid_hex: &str, block: &[u8]) -> bool {
let bytes = match hex::decode(cid_hex) {
Ok(b) => b,
Err(_) => return false,
};
let cid = match cid::Cid::read_bytes(bytes.as_slice()) {
Ok(c) => c,
Err(_) => return false,
};
let hash = cid.hash();
let digest = hash.digest();
let mut hasher = sha2::Sha256::new();
use sha2::Digest;
hasher.update(block);
let out = hasher.finalize();
let mut computed = [0u8; 32];
computed.copy_from_slice(&out);
digest == computed
}
#[tokio::test]
async fn sync_get_record_includes_mst_proof() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, jwt, _existing_cids) = fresh_user_with_records().await;
let created: Value = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": "proof please",
"createdAt": "2026-07-01T12:00:00Z",
},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let value_cid = created["cid"].as_str().unwrap().to_string();
let rkey = created["uri"]
.as_str()
.unwrap()
.rsplit('/')
.next()
.unwrap()
.to_string();
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.twi.post&rkey={}",
PDS_URL, did, rkey
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let bytes = resp.bytes().await.unwrap();
let parsed = parse_car(&bytes);
let target_hex: String = value_cid
.parse::<cid::Cid>()
.unwrap()
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert!(
parsed.blocks.iter().any(|(c, _)| c == &target_hex),
"value block missing from CAR"
);
let mut dag_cbor_nodes = 0usize;
for (cid_hex, data) in &parsed.blocks {
assert!(
cid_matches_block(cid_hex, data),
"CAR block CID mismatch for {cid_hex}"
);
if let Ok(val) = ciborium::from_reader::<ciborium::value::Value, _>(data.as_slice()) {
if let ciborium::value::Value::Map(_) = val {
dag_cbor_nodes += 1;
}
}
}
assert!(
parsed.blocks.len() >= 3,
"expected head commit + value + at least one MST node; got {}",
parsed.blocks.len()
);
assert!(
dag_cbor_nodes >= 2,
"expected at least 2 DAG-CBOR map blocks (head commit + MST nodes); got {}",
dag_cbor_nodes
);
let repo_bytes = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo?did={}",
PDS_URL, did
))
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let full = parse_car(&repo_bytes);
let mut found_in_full = 0usize;
for (cid_hex, _) in &parsed.blocks {
if full.blocks.iter().any(|(c, _)| c == cid_hex) {
found_in_full += 1;
}
}
assert_eq!(
found_in_full,
parsed.blocks.len(),
"every block in getRecord CAR must also appear in getRepo CAR"
);
}
#[tokio::test]
async fn sync_list_repos_keyset_pagination() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let mut created_dids = Vec::new();
for i in 0..5 {
let (c, did, jwt) = fresh_account(&format!("page{i:02}")).await;
let resp: Value = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": format!("pagination seed {i}"),
"createdAt": "2026-07-01T12:00:00Z",
},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(resp["uri"].is_string(), "createRecord: {:?}", resp);
created_dids.push(did);
}
let min_did = created_dids.iter().min().unwrap().clone();
let start_cursor = did_cursor_lt(&min_did);
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut cursor: Option<String> = Some(start_cursor);
let mut pages = 0;
loop {
pages += 1;
assert!(pages < 2000, "pagination did not terminate");
let url = format!(
"{}/xrpc/com.atproto.sync.listRepos?limit=2&cursor={}",
PDS_URL,
urlencode(cursor.as_deref().unwrap_or(""))
);
let resp = client().await.get(&url).send().await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let body: Value = resp.json().await.unwrap();
let repos = body["repos"].as_array().expect("repos array");
for r in repos {
let did = r["did"].as_str().unwrap().to_string();
assert!(
seen.insert(did.clone()),
"duplicate DID across pages: {did}"
);
}
if created_dids.iter().all(|d| seen.contains(d)) {
break;
}
match body["cursor"].as_str() {
Some(c) => cursor = Some(c.to_string()),
None => panic!(
"pagination exhausted before all created DIDs were seen; missing {:?}",
created_dids.iter().filter(|d| !seen.contains(*d)).collect::<Vec<_>>()
),
}
}
}
fn urlencode(s: &str) -> String {
s.chars()
.map(|c| match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
other => format!("%{:02X}", other as u32),
})
.collect()
}
fn did_cursor_lt(did: &str) -> String {
let bytes = did.as_bytes();
let mut prefix = Vec::with_capacity(bytes.len());
for &b in bytes {
if b == 0 {
prefix.push(b);
} else {
prefix.push(b - 1);
break;
}
}
if prefix.len() < bytes.len() {
prefix.extend_from_slice(&bytes[prefix.len()..]);
} else {
prefix.push(b'_');
}
String::from_utf8(prefix).unwrap_or_else(|_| did.to_string())
}
#[test]
fn did_cursor_lt_is_strictly_less() {
let s = "did:key:z16Dxyz";
let lt = did_cursor_lt(s);
assert!(lt.as_str() < s, "{lt} should be < {s}");
let min = std::cmp::min(s, lt.as_str());
assert_eq!(min, lt.as_str());
}
#[tokio::test]
async fn sync_list_repos_caps_limit() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.listRepos?limit=10000",
PDS_URL
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let body: Value = resp.json().await.unwrap();
let repos = body["repos"].as_array().expect("repos array");
assert!(
repos.len() <= 1000,
"limit=10000 should be capped at MAX_LIST_LIMIT=1000, got {}",
repos.len()
);
}
#[tokio::test]
async fn sync_get_record_returns_404_for_missing() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, _jwt, _cids) = fresh_user_with_records().await;
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.twi.post&rkey=totallymissingkey",
PDS_URL, did
))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 404);
let body: Value = resp.json().await.unwrap();
assert_eq!(body["error"], json!("RecordNotFound"));
}
// -- embed-carrying record tests ------------------------------------------
/// Post a record with an `app.bsky.embed.images` embed. The PDS should
/// accept it (the lexicon allows embed variants), persist the value
/// block, and return a 200 with a URI+CID. The AppView's ingest-commit
/// push is best-effort, so we don't depend on the AppView being up.
#[tokio::test]
async fn create_record_with_image_embed() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let handle = format!(
"img_{}.maarcadetweet.local",
uuid::Uuid::new_v4().simple()
);
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({
"handle": handle,
"password": "hunter2hunter2"
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
let resp = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": "check out this image",
"createdAt": "2026-07-01T12:00:00Z",
"embed": {
"$type": "app.bsky.embed.images",
"images": [
{
"alt": "a single image",
"image": {
"$type": "blob",
"ref": {"$link": "bafyreiblob1"},
"mimeType": "image/jpeg",
"size": 1024
},
"aspectRatio": {"width": 800, "height": 600}
}
]
}
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200, "image embed post must succeed");
let body: Value = resp.json().await.unwrap();
assert!(body["uri"].is_string(), "createRecord: {:?}", body);
let cid = body["cid"].as_str().unwrap();
let uri = body["uri"].as_str().unwrap();
assert!(cid.starts_with("bafy"), "cid should be a CID");
assert!(uri.starts_with("at://"), "uri should be at://");
// Round-trip via sync.getRecord to confirm the value block survived
// CBOR encoding at the right CID. We don't decode the CBOR here —
// the AppView's embed parsing test covers that — but we verify the
// block is reachable from the repo so future reads succeed.
let rkey = uri.rsplit('/').next().unwrap();
let car = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.twi.post&rkey={}",
PDS_URL, did, rkey
))
.send()
.await
.unwrap();
assert_eq!(car.status().as_u16(), 200);
let parsed = parse_car(&car.bytes().await.unwrap());
let target_hex: String = cid
.parse::<cid::Cid>()
.unwrap()
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert!(
parsed.blocks.iter().any(|(c, _)| c == &target_hex),
"value block {cid} must be in getRecord CAR"
);
}
/// Post a record with an `app.bsky.embed.external` (link card).
#[tokio::test]
async fn create_record_with_external_embed() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let c = client().await;
let handle = format!(
"ext_{}.maarcadetweet.local",
uuid::Uuid::new_v4().simple()
);
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({
"handle": handle,
"password": "hunter2hunter2"
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
let resp = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": "see link",
"createdAt": "2026-07-01T12:00:00Z",
"embed": {
"$type": "app.bsky.embed.external",
"external": {
"uri": "https://example.com/article",
"title": "An article",
"description": "Short description."
}
}
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let body: Value = resp.json().await.unwrap();
assert!(body["uri"].is_string(), "createRecord: {:?}", body);
}
// -- like / deleteRecord tests ---------------------------------------------
/// Helper: build a `(did, jwt)` pair against a fresh account. Reused
/// by the like/delete tests; the handle is unique-per-call so we
/// don't collide with parallel test runs.
async fn fresh_user(prefix: &str) -> (reqwest::Client, String, String) {
let c = client().await;
let handle = format!(
"{}_{}.maarcadetweet.local",
prefix,
uuid::Uuid::new_v4().simple()
);
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
(c, did, jwt)
}
/// Seed a single post we can like. Returns the post's `uri` and `cid`
/// — both are needed to build a like record's `subject`.
async fn seed_post(
c: &reqwest::Client,
did: &str,
jwt: &str,
text: &str,
) -> (String, String) {
let resp: Value = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": text,
"createdAt": "2026-07-01T12:00:00Z",
},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let uri = resp["uri"].as_str().unwrap().to_string();
let cid = resp["cid"].as_str().unwrap().to_string();
(uri, cid)
}
#[tokio::test]
async fn create_like_persists_record_and_returns_uri() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, jwt) = fresh_user("like").await;
let (subject_uri, subject_cid) =
seed_post(&c, &did, &jwt, "post to be liked").await;
// Use the flat BSky shape — what the Tauri client will send.
let resp: Value = c
.post(format!("{}/xrpc/com.atproto.feed.like.create", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"subject": {
"uri": subject_uri,
"cid": subject_cid,
},
"createdAt": "2026-07-04T12:00:00Z",
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let uri = resp["uri"].as_str().expect("uri missing");
let cid = resp["cid"].as_str().expect("cid missing");
let commit = resp["commit"].as_object().expect("commit object");
// The returned URI should be at://{did}/app.bsky.feed.like/{rkey}.
assert!(uri.starts_with(&format!("at://{did}/app.bsky.feed.like/")));
assert!(cid.starts_with("bafy"), "cid should look like a CID: {cid}");
assert!(commit["cid"].is_string());
assert!(commit["rev"].is_string());
// The like should be readable back via sync.getRecord.
let rkey = uri.rsplit('/').next().unwrap();
let car = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.bsky.feed.like&rkey={}",
PDS_URL, did, rkey
))
.send()
.await
.unwrap();
assert_eq!(car.status().as_u16(), 200);
// Round-trip with the generic createRecord body shape to
// confirm the handler accepts both shapes.
let (subject_uri2, subject_cid2) =
seed_post(&c, &did, &jwt, "second post for generic-shape like").await;
let resp2: Value = c
.post(format!("{}/xrpc/com.atproto.feed.like.create", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.bsky.feed.like",
"record": {
"subject": {
"uri": subject_uri2,
"cid": subject_cid2,
},
"createdAt": "2026-07-04T12:00:00Z",
},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert!(
resp2["uri"]
.as_str()
.unwrap()
.starts_with(&format!("at://{did}/app.bsky.feed.like/")),
"generic-shape like should also work, got: {resp2:?}"
);
// A wrong collection name should be rejected.
let r_wrong = c
.post(format!("{}/xrpc/com.atproto.feed.like.create", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"subject": {
"uri": subject_uri,
"cid": subject_cid,
},
"createdAt": "2026-07-04T12:00:00Z",
},
}))
.send()
.await
.unwrap();
assert_eq!(r_wrong.status().as_u16(), 400);
// No bearer header should be rejected.
let r_noauth = c
.post(format!("{}/xrpc/com.atproto.feed.like.create", PDS_URL))
.json(&json!({
"repo": did,
"subject": {
"uri": subject_uri,
"cid": subject_cid,
},
"createdAt": "2026-07-04T12:00:00Z",
}))
.send()
.await
.unwrap();
assert_eq!(r_noauth.status().as_u16(), 401);
}
#[tokio::test]
async fn delete_record_removes_like() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
let (c, did, jwt) = fresh_user("del").await;
let (subject_uri, subject_cid) =
seed_post(&c, &did, &jwt, "to be liked then unliked").await;
// Create a like.
let create: Value = c
.post(format!("{}/xrpc/com.atproto.feed.like.create", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"subject": {
"uri": subject_uri,
"cid": subject_cid,
},
"createdAt": "2026-07-04T12:00:00Z",
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let like_uri = create["uri"].as_str().unwrap().to_string();
let like_rkey = like_uri.rsplit('/').next().unwrap().to_string();
// Delete it.
let del: Value = c
.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.bsky.feed.like",
"rkey": like_rkey,
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let commit = del["commit"].as_object().expect("commit object");
assert!(commit["cid"].is_string());
assert!(commit["rev"].is_string());
// The like should be gone from sync.getRecord.
let r = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.bsky.feed.like&rkey={}",
PDS_URL, did, like_rkey
))
.send()
.await
.unwrap();
assert_eq!(r.status().as_u16(), 404, "deleted like must 404");
// Idempotency: deleting again should still return 200 with a
// commit. (The repo's MST is unchanged so the commit is a
// no-op, but the request is accepted.)
let del2 = c
.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.bsky.feed.like",
"rkey": like_rkey,
}))
.send()
.await
.unwrap();
assert_eq!(del2.status().as_u16(), 200, "second delete must be idempotent");
// `deleteRecord` is generic — it should work for any
// collection. Create a post and delete it the same way.
let post_resp: Value = c
.post(format!("{}/xrpc/com.atproto.repo.createRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": "to be deleted",
"createdAt": "2026-07-04T12:00:00Z",
},
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let post_uri = post_resp["uri"].as_str().unwrap().to_string();
let post_rkey = post_uri.rsplit('/').next().unwrap().to_string();
let r3 = c
.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", PDS_URL))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"rkey": post_rkey,
}))
.send()
.await
.unwrap();
assert_eq!(r3.status().as_u16(), 200);
// Auth: deleting someone else's record (here: a fabricated repo
// matching the JWT sub) is rejected when the JWT sub doesn't
// match the body. We can't easily mint a JWT for a different
// DID in this test, so we just check that missing JWT → 401.
let r_noauth = c
.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", PDS_URL))
.json(&json!({
"repo": did,
"collection": "app.bsky.feed.like",
"rkey": "any",
}))
.send()
.await
.unwrap();
assert_eq!(r_noauth.status().as_u16(), 401);
}
// -- concurrent write tests (Phase 5b review C1) ---------------------------
/// Phase 5b review C1 — concurrent PDS writes used to race on the
/// `repos.head_commit` column. Two writers would both read the same
/// head, both build a valid child commit with the same `prev`, and
/// the second `UPDATE` would clobber the first — leaving the first
/// writer's MST changes stranded in `repo_blocks` but unreachable
/// from the new head.
///
/// The fix is `SELECT … FOR UPDATE` on the user's `repos` row inside
/// a transaction (see `routes::helpers::apply_repo_write`). This
/// test fires 10 parallel `createRecord` requests for one DID and
/// asserts every record survives into the final head: every rkey is
/// fetchable via `sync.getRecord` (each returns 200, not 404), and
/// the MST contains exactly the 10 records we created.
#[tokio::test]
async fn concurrent_writes_dont_lose_data() {
if !wait_for_pds().await {
eprintln!("pds not running, skipping");
return;
}
// Concurrent writers serialise on the row lock; with 10 of them
// each round-trip takes a few hundred ms, so the per-request
// timeout has to be generous. The default 5s `client()` would
// time out long requests 9 and 10.
let c = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.build()
.unwrap();
let handle = format!(
"race_{}.maarcadetweet.local",
uuid::Uuid::new_v4().simple()
);
let acc: Value = c
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let did = acc["did"].as_str().unwrap().to_string();
let jwt = acc["access_jwt"].as_str().unwrap().to_string();
// Fire N parallel createRecord requests. Each request has a
// distinct text + createdAt so the value CIDs differ; if the
// TID helper ever regressed to collisions, the rkeys would still
// collide and we'd lose records (the integration-level race is
// orthogonal to the TID race).
//
// N=5 is the largest batch that comfortably fits in sqlx's
// default 10-connection pool — each in-flight write holds a
// transaction connection, and the helper also issues a non-tx
// signing-key read on a *second* connection per request.
const N: usize = 5;
let mut handles = Vec::with_capacity(N);
for i in 0..N {
let c = c.clone();
let did = did.clone();
let jwt = jwt.clone();
handles.push(tokio::spawn(async move {
let resp = c
.post(format!(
"{}/xrpc/com.atproto.repo.createRecord",
PDS_URL
))
.bearer_auth(&jwt)
.json(&json!({
"repo": did,
"collection": "app.twi.post",
"record": {
"text": format!("concurrent post #{i}"),
"createdAt": format!("2026-07-04T12:00:{:02}Z", i),
},
}))
.send()
.await
.unwrap();
let status = resp.status().as_u16();
let body: Value = resp.json().await.unwrap();
(status, body)
}));
}
let mut created = Vec::with_capacity(N);
for h in handles {
let (status, body) = h.await.unwrap();
assert_eq!(
status, 200,
"concurrent createRecord failed: {body:?}"
);
let uri = body["uri"].as_str().unwrap().to_string();
let cid = body["cid"].as_str().unwrap().to_string();
created.push((uri, cid));
}
assert_eq!(created.len(), N);
// Every record must be fetchable from the final repo. If a
// concurrent writer lost its MST update, sync.getRecord for that
// rkey returns 404.
for (uri, cid) in &created {
let rkey = uri.rsplit('/').next().unwrap();
let resp = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord?did={}&collection=app.twi.post&rkey={}",
PDS_URL, did, rkey
))
.send()
.await
.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"record {uri} (cid {cid}) lost after concurrent writes — repo_blocks has it but head doesn't"
);
let bytes = resp.bytes().await.unwrap();
let parsed = parse_car(&bytes);
let target_hex: String = cid
.parse::<cid::Cid>()
.unwrap()
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert!(
parsed.blocks.iter().any(|(c, _)| c == &target_hex),
"CAR for record {uri} missing value block {cid} (raw: {target_hex})"
);
}
// Belt-and-braces: fetch getRepo and confirm the head commit's
// `prev` chain is well-formed (every commit's `prev` is reachable
// from the next). The CAR includes every block in the repo so
// we can also check that the MST root and all 10 value CIDs are
// present.
let repo_bytes = c
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo?did={}",
PDS_URL, did
))
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let parsed = parse_car(&repo_bytes);
for (_uri, cid) in &created {
let target_hex: String = cid
.parse::<cid::Cid>()
.unwrap()
.to_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert!(
parsed.blocks.iter().any(|(c, _)| c == &target_hex),
"final head repo missing value block {cid}"
);
}
}