mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-19 00:34:15 +00:00
First version of pds migration
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_repo_requires_auth() {
|
||||
let client = client();
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(vec![0u8; 100])
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_repo_invalid_car() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(vec![0u8; 100])
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_repo_empty_body() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_repo_with_exported_repo() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let post_payload = json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"record": {
|
||||
"$type": "app.bsky.feed.post",
|
||||
"text": "Test post for import",
|
||||
"createdAt": chrono::Utc::now().to_rfc3339(),
|
||||
}
|
||||
});
|
||||
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&post_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create post");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
|
||||
let export_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.sync.getRepo?did={}",
|
||||
base_url().await,
|
||||
did
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to export repo");
|
||||
assert_eq!(export_res.status(), StatusCode::OK);
|
||||
|
||||
let car_bytes = export_res.bytes().await.expect("Failed to get CAR bytes");
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to import repo");
|
||||
|
||||
assert_eq!(import_res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use iroh_car::CarHeader;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
|
||||
loop {
|
||||
let mut byte = (value & 0x7F) as u8;
|
||||
value >>= 7;
|
||||
if value != 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
buf.push(byte);
|
||||
if value == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_rejects_car_for_different_user() {
|
||||
let client = client();
|
||||
|
||||
let (token_a, did_a) = create_account_and_login(&client).await;
|
||||
let (_token_b, did_b) = create_account_and_login(&client).await;
|
||||
|
||||
let export_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.sync.getRepo?did={}",
|
||||
base_url().await,
|
||||
did_b
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Export failed");
|
||||
|
||||
assert_eq!(export_res.status(), StatusCode::OK);
|
||||
let car_bytes = export_res.bytes().await.unwrap();
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token_a)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.expect("Import failed");
|
||||
|
||||
assert_eq!(import_res.status(), StatusCode::FORBIDDEN);
|
||||
let body: serde_json::Value = import_res.json().await.unwrap();
|
||||
assert!(
|
||||
body["error"] == "InvalidRequest" || body["error"] == "DidMismatch",
|
||||
"Expected DidMismatch or InvalidRequest error, got: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_accepts_own_exported_repo() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let post_payload = json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"record": {
|
||||
"$type": "app.bsky.feed.post",
|
||||
"text": "Original post before export",
|
||||
"createdAt": chrono::Utc::now().to_rfc3339(),
|
||||
}
|
||||
});
|
||||
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&post_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create post");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
|
||||
let export_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.sync.getRepo?did={}",
|
||||
base_url().await,
|
||||
did
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to export repo");
|
||||
assert_eq!(export_res.status(), StatusCode::OK);
|
||||
let car_bytes = export_res.bytes().await.unwrap();
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to import repo");
|
||||
|
||||
assert_eq!(import_res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_repo_size_limit() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let oversized_body = vec![0u8; 110 * 1024 * 1024];
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(oversized_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(response) => {
|
||||
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
Err(e) => {
|
||||
let error_str = e.to_string().to_lowercase();
|
||||
assert!(
|
||||
error_str.contains("broken pipe") ||
|
||||
error_str.contains("connection") ||
|
||||
error_str.contains("reset") ||
|
||||
error_str.contains("request") ||
|
||||
error_str.contains("body"),
|
||||
"Expected connection error or PAYLOAD_TOO_LARGE, got: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_deactivated_account_rejected() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let export_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.sync.getRepo?did={}",
|
||||
base_url().await,
|
||||
did
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Export failed");
|
||||
assert_eq!(export_res.status(), StatusCode::OK);
|
||||
let car_bytes = export_res.bytes().await.unwrap();
|
||||
|
||||
let deactivate_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.deactivateAccount",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Deactivate failed");
|
||||
assert!(deactivate_res.status().is_success());
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.expect("Import failed");
|
||||
|
||||
assert!(
|
||||
import_res.status() == StatusCode::FORBIDDEN || import_res.status() == StatusCode::UNAUTHORIZED,
|
||||
"Expected FORBIDDEN (403) or UNAUTHORIZED (401), got {}",
|
||||
import_res.status()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_invalid_car_structure() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(invalid_car)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_car_with_no_roots() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let header = CarHeader::new_v1(vec![]);
|
||||
let header_cbor = header.encode().unwrap_or_default();
|
||||
let mut car = Vec::new();
|
||||
write_varint(&mut car, header_cbor.len() as u64);
|
||||
car.extend_from_slice(&header_cbor);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_preserves_records_after_reimport() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let mut rkeys = Vec::new();
|
||||
for i in 0..3 {
|
||||
let post_payload = json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"record": {
|
||||
"$type": "app.bsky.feed.post",
|
||||
"text": format!("Test post {}", i),
|
||||
"createdAt": chrono::Utc::now().to_rfc3339(),
|
||||
}
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&post_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create post");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
let uri = body["uri"].as_str().unwrap();
|
||||
let rkey = uri.split('/').last().unwrap().to_string();
|
||||
rkeys.push(rkey);
|
||||
}
|
||||
|
||||
for rkey in &rkeys {
|
||||
let get_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
|
||||
base_url().await,
|
||||
did,
|
||||
rkey
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to get record before export");
|
||||
assert_eq!(get_res.status(), StatusCode::OK, "Record {} not found before export", rkey);
|
||||
}
|
||||
|
||||
let export_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.sync.getRepo?did={}",
|
||||
base_url().await,
|
||||
did
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to export repo");
|
||||
assert_eq!(export_res.status(), StatusCode::OK);
|
||||
let car_bytes = export_res.bytes().await.unwrap();
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to import repo");
|
||||
assert_eq!(import_res.status(), StatusCode::OK);
|
||||
|
||||
let list_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
|
||||
base_url().await,
|
||||
did
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list records after import");
|
||||
assert_eq!(list_res.status(), StatusCode::OK);
|
||||
let list_body: serde_json::Value = list_res.json().await.unwrap();
|
||||
let records_after = list_body["records"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
|
||||
assert!(
|
||||
records_after >= 1,
|
||||
"Expected at least 1 record after import, found {}. Note: MST walk may have timing issues.",
|
||||
records_after
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use cid::Cid;
|
||||
use ipld_core::ipld::Ipld;
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::PgPool;
|
||||
use std::collections::BTreeMap;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
fn make_cid(data: &[u8]) -> Cid {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
let hash = hasher.finalize();
|
||||
let multihash = multihash::Multihash::wrap(0x12, &hash).unwrap();
|
||||
Cid::new_v1(0x71, multihash)
|
||||
}
|
||||
|
||||
fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
|
||||
loop {
|
||||
let mut byte = (value & 0x7F) as u8;
|
||||
value >>= 7;
|
||||
if value != 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
buf.push(byte);
|
||||
if value == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_car_block(cid: &Cid, data: &[u8]) -> Vec<u8> {
|
||||
let cid_bytes = cid.to_bytes();
|
||||
let mut result = Vec::new();
|
||||
write_varint(&mut result, (cid_bytes.len() + data.len()) as u64);
|
||||
result.extend_from_slice(&cid_bytes);
|
||||
result.extend_from_slice(data);
|
||||
result
|
||||
}
|
||||
|
||||
fn get_multikey_from_signing_key(signing_key: &SigningKey) -> String {
|
||||
let public_key = signing_key.verifying_key();
|
||||
let compressed = public_key.to_sec1_bytes();
|
||||
|
||||
fn encode_uvarint(mut x: u64) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
while x >= 0x80 {
|
||||
out.push(((x as u8) & 0x7F) | 0x80);
|
||||
x >>= 7;
|
||||
}
|
||||
out.push(x as u8);
|
||||
out
|
||||
}
|
||||
|
||||
let mut buf = encode_uvarint(0xE7);
|
||||
buf.extend_from_slice(&compressed);
|
||||
multibase::encode(multibase::Base::Base58Btc, buf)
|
||||
}
|
||||
|
||||
fn create_did_document(did: &str, handle: &str, signing_key: &SigningKey, pds_endpoint: &str) -> serde_json::Value {
|
||||
let multikey = get_multikey_from_signing_key(signing_key);
|
||||
|
||||
json!({
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/multikey/v1"
|
||||
],
|
||||
"id": did,
|
||||
"alsoKnownAs": [format!("at://{}", handle)],
|
||||
"verificationMethod": [{
|
||||
"id": format!("{}#atproto", did),
|
||||
"type": "Multikey",
|
||||
"controller": did,
|
||||
"publicKeyMultibase": multikey
|
||||
}],
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"serviceEndpoint": pds_endpoint
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
fn create_signed_commit(
|
||||
did: &str,
|
||||
data_cid: &Cid,
|
||||
signing_key: &SigningKey,
|
||||
) -> (Vec<u8>, Cid) {
|
||||
let rev = Tid::now(LimitedU32::MIN).to_string();
|
||||
|
||||
let unsigned = Ipld::Map(BTreeMap::from([
|
||||
("data".to_string(), Ipld::Link(*data_cid)),
|
||||
("did".to_string(), Ipld::String(did.to_string())),
|
||||
("prev".to_string(), Ipld::Null),
|
||||
("rev".to_string(), Ipld::String(rev.clone())),
|
||||
("sig".to_string(), Ipld::Bytes(vec![])),
|
||||
("version".to_string(), Ipld::Integer(3)),
|
||||
]));
|
||||
|
||||
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap();
|
||||
|
||||
let signature: Signature = signing_key.sign(&unsigned_bytes);
|
||||
let sig_bytes = signature.to_bytes().to_vec();
|
||||
|
||||
let signed = Ipld::Map(BTreeMap::from([
|
||||
("data".to_string(), Ipld::Link(*data_cid)),
|
||||
("did".to_string(), Ipld::String(did.to_string())),
|
||||
("prev".to_string(), Ipld::Null),
|
||||
("rev".to_string(), Ipld::String(rev)),
|
||||
("sig".to_string(), Ipld::Bytes(sig_bytes)),
|
||||
("version".to_string(), Ipld::Integer(3)),
|
||||
]));
|
||||
|
||||
let signed_bytes = serde_ipld_dagcbor::to_vec(&signed).unwrap();
|
||||
let cid = make_cid(&signed_bytes);
|
||||
|
||||
(signed_bytes, cid)
|
||||
}
|
||||
|
||||
fn create_mst_node(entries: Vec<(String, Cid)>) -> (Vec<u8>, Cid) {
|
||||
let ipld_entries: Vec<Ipld> = entries
|
||||
.into_iter()
|
||||
.map(|(key, value_cid)| {
|
||||
Ipld::Map(BTreeMap::from([
|
||||
("k".to_string(), Ipld::Bytes(key.into_bytes())),
|
||||
("v".to_string(), Ipld::Link(value_cid)),
|
||||
("p".to_string(), Ipld::Integer(0)),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let node = Ipld::Map(BTreeMap::from([
|
||||
("e".to_string(), Ipld::List(ipld_entries)),
|
||||
]));
|
||||
|
||||
let bytes = serde_ipld_dagcbor::to_vec(&node).unwrap();
|
||||
let cid = make_cid(&bytes);
|
||||
(bytes, cid)
|
||||
}
|
||||
|
||||
fn create_record() -> (Vec<u8>, Cid) {
|
||||
let record = Ipld::Map(BTreeMap::from([
|
||||
("$type".to_string(), Ipld::String("app.bsky.feed.post".to_string())),
|
||||
("text".to_string(), Ipld::String("Test post for verification".to_string())),
|
||||
("createdAt".to_string(), Ipld::String("2024-01-01T00:00:00Z".to_string())),
|
||||
]));
|
||||
|
||||
let bytes = serde_ipld_dagcbor::to_vec(&record).unwrap();
|
||||
let cid = make_cid(&bytes);
|
||||
(bytes, cid)
|
||||
}
|
||||
|
||||
fn build_car_with_signature(
|
||||
did: &str,
|
||||
signing_key: &SigningKey,
|
||||
) -> (Vec<u8>, Cid) {
|
||||
let (record_bytes, record_cid) = create_record();
|
||||
|
||||
let (mst_bytes, mst_cid) = create_mst_node(vec![
|
||||
("app.bsky.feed.post/test123".to_string(), record_cid),
|
||||
]);
|
||||
|
||||
let (commit_bytes, commit_cid) = create_signed_commit(did, &mst_cid, signing_key);
|
||||
|
||||
let header = iroh_car::CarHeader::new_v1(vec![commit_cid]);
|
||||
let header_bytes = header.encode().unwrap();
|
||||
|
||||
let mut car = Vec::new();
|
||||
write_varint(&mut car, header_bytes.len() as u64);
|
||||
car.extend_from_slice(&header_bytes);
|
||||
car.extend(encode_car_block(&commit_cid, &commit_bytes));
|
||||
car.extend(encode_car_block(&mst_cid, &mst_bytes));
|
||||
car.extend(encode_car_block(&record_cid, &record_bytes));
|
||||
|
||||
(car, commit_cid)
|
||||
}
|
||||
|
||||
async fn setup_mock_plc_directory(did: &str, did_doc: serde_json::Value) -> MockServer {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
let did_encoded = urlencoding::encode(did);
|
||||
let did_path = format!("/{}", did_encoded);
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(did_path))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(did_doc))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
mock_server
|
||||
}
|
||||
|
||||
async fn get_user_signing_key(did: &str) -> Option<Vec<u8>> {
|
||||
let db_url = get_db_connection_string().await;
|
||||
let pool = PgPool::connect(&db_url).await.ok()?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT k.key_bytes, k.encryption_version
|
||||
FROM user_keys k
|
||||
JOIN users u ON k.user_id = u.id
|
||||
WHERE u.did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.ok()??;
|
||||
|
||||
bspds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_with_valid_signature_and_mock_plc() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let key_bytes = get_user_signing_key(&did).await
|
||||
.expect("Failed to get user signing key");
|
||||
let signing_key = SigningKey::from_slice(&key_bytes)
|
||||
.expect("Failed to create signing key");
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
|
||||
let handle = did.split(':').last().unwrap_or("user");
|
||||
let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
|
||||
|
||||
let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
|
||||
std::env::remove_var("SKIP_IMPORT_VERIFICATION");
|
||||
}
|
||||
|
||||
let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes)
|
||||
.send()
|
||||
.await
|
||||
.expect("Import request failed");
|
||||
|
||||
let status = import_res.status();
|
||||
let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::OK,
|
||||
"Import with valid signature should succeed. Response: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_with_wrong_signing_key_fails() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let wrong_signing_key = SigningKey::random(&mut rand::thread_rng());
|
||||
|
||||
let key_bytes = get_user_signing_key(&did).await
|
||||
.expect("Failed to get user signing key");
|
||||
let correct_signing_key = SigningKey::from_slice(&key_bytes)
|
||||
.expect("Failed to create signing key");
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
|
||||
let handle = did.split(':').last().unwrap_or("user");
|
||||
let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint);
|
||||
|
||||
let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
|
||||
std::env::remove_var("SKIP_IMPORT_VERIFICATION");
|
||||
}
|
||||
|
||||
let (car_bytes, _root_cid) = build_car_with_signature(&did, &wrong_signing_key);
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes)
|
||||
.send()
|
||||
.await
|
||||
.expect("Import request failed");
|
||||
|
||||
let status = import_res.status();
|
||||
let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Import with wrong signature should fail. Response: {:?}",
|
||||
body
|
||||
);
|
||||
assert!(
|
||||
body["error"] == "InvalidSignature" || body["message"].as_str().unwrap_or("").contains("signature"),
|
||||
"Error should mention signature: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_with_did_mismatch_fails() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let key_bytes = get_user_signing_key(&did).await
|
||||
.expect("Failed to get user signing key");
|
||||
let signing_key = SigningKey::from_slice(&key_bytes)
|
||||
.expect("Failed to create signing key");
|
||||
|
||||
let wrong_did = "did:plc:wrongdidthatdoesnotmatch";
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
|
||||
let handle = did.split(':').last().unwrap_or("user");
|
||||
let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
|
||||
|
||||
let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
|
||||
std::env::remove_var("SKIP_IMPORT_VERIFICATION");
|
||||
}
|
||||
|
||||
let (car_bytes, _root_cid) = build_car_with_signature(wrong_did, &signing_key);
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes)
|
||||
.send()
|
||||
.await
|
||||
.expect("Import request failed");
|
||||
|
||||
let status = import_res.status();
|
||||
let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::FORBIDDEN,
|
||||
"Import with DID mismatch should be forbidden. Response: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_with_plc_resolution_failure() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let key_bytes = get_user_signing_key(&did).await
|
||||
.expect("Failed to get user signing key");
|
||||
let signing_key = SigningKey::from_slice(&key_bytes)
|
||||
.expect("Failed to create signing key");
|
||||
|
||||
let mock_plc = MockServer::start().await;
|
||||
|
||||
let did_encoded = urlencoding::encode(&did);
|
||||
let did_path = format!("/{}", did_encoded);
|
||||
Mock::given(method("GET"))
|
||||
.and(path(did_path))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&mock_plc)
|
||||
.await;
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
|
||||
std::env::remove_var("SKIP_IMPORT_VERIFICATION");
|
||||
}
|
||||
|
||||
let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes)
|
||||
.send()
|
||||
.await
|
||||
.expect("Import request failed");
|
||||
|
||||
let status = import_res.status();
|
||||
let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Import with PLC resolution failure should fail. Response: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_with_no_signing_key_in_did_doc() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let key_bytes = get_user_signing_key(&did).await
|
||||
.expect("Failed to get user signing key");
|
||||
let signing_key = SigningKey::from_slice(&key_bytes)
|
||||
.expect("Failed to create signing key");
|
||||
|
||||
let handle = did.split(':').last().unwrap_or("user");
|
||||
let did_doc_without_key = json!({
|
||||
"@context": ["https://www.w3.org/ns/did/v1"],
|
||||
"id": did,
|
||||
"alsoKnownAs": [format!("at://{}", handle)],
|
||||
"verificationMethod": [],
|
||||
"service": []
|
||||
});
|
||||
|
||||
let mock_plc = setup_mock_plc_directory(&did, did_doc_without_key).await;
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
|
||||
std::env::remove_var("SKIP_IMPORT_VERIFICATION");
|
||||
}
|
||||
|
||||
let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
|
||||
|
||||
let import_res = client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
|
||||
.bearer_auth(&token)
|
||||
.header("Content-Type", "application/vnd.ipld.car")
|
||||
.body(car_bytes)
|
||||
.send()
|
||||
.await
|
||||
.expect("Import request failed");
|
||||
|
||||
let status = import_res.status();
|
||||
let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Import with missing signing key should fail. Response: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,491 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_plc_operation_signature_requires_auth() {
|
||||
let client = client();
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
|
||||
base_url().await
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_plc_operation_signature_success() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sign_plc_operation_requires_auth() {
|
||||
let client = client();
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.signPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.json(&json!({}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sign_plc_operation_requires_token() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.signPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sign_plc_operation_invalid_token() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.signPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"token": "invalid-token-12345"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert!(body["error"] == "InvalidToken" || body["error"] == "ExpiredToken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_requires_auth() {
|
||||
let client = client();
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.json(&json!({
|
||||
"operation": {}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_invalid_operation() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"operation": {
|
||||
"type": "invalid_type"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_missing_sig() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"operation": {
|
||||
"type": "plc_operation",
|
||||
"rotationKeys": [],
|
||||
"verificationMethods": {},
|
||||
"alsoKnownAs": [],
|
||||
"services": {},
|
||||
"prev": null
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_wrong_service_endpoint() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"operation": {
|
||||
"type": "plc_operation",
|
||||
"rotationKeys": ["did:key:z123"],
|
||||
"verificationMethods": {"atproto": "did:key:z456"},
|
||||
"alsoKnownAs": ["at://wrong.handle"],
|
||||
"services": {
|
||||
"atproto_pds": {
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"endpoint": "https://wrong.example.com"
|
||||
}
|
||||
},
|
||||
"prev": null,
|
||||
"sig": "fake_signature"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_plc_operation_creates_token_in_db() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let db_url = get_db_connection_string().await;
|
||||
let pool = PgPool::connect(&db_url).await.expect("DB connect failed");
|
||||
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT t.token, t.expires_at
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.expect("Query failed");
|
||||
|
||||
assert!(row.is_some(), "PLC token should be created in database");
|
||||
let row = row.unwrap();
|
||||
assert!(row.token.len() == 11, "Token should be in format xxxxx-xxxxx");
|
||||
assert!(row.token.contains('-'), "Token should contain hyphen");
|
||||
assert!(row.expires_at > chrono::Utc::now(), "Token should not be expired");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_plc_operation_replaces_existing_token() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let res1 = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request 1 failed");
|
||||
assert_eq!(res1.status(), StatusCode::OK);
|
||||
|
||||
let db_url = get_db_connection_string().await;
|
||||
let pool = PgPool::connect(&db_url).await.expect("DB connect failed");
|
||||
|
||||
let token1 = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT t.token
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Query failed");
|
||||
|
||||
let res2 = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request 2 failed");
|
||||
assert_eq!(res2.status(), StatusCode::OK);
|
||||
|
||||
let token2 = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT t.token
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Query failed");
|
||||
|
||||
assert_ne!(token1, token2, "Second request should generate a new token");
|
||||
|
||||
let count: i64 = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*) as "count!"
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Count query failed");
|
||||
|
||||
assert_eq!(count, 1, "Should only have one token per user");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_wrong_verification_method() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
|
||||
format!("127.0.0.1:{}", app_port())
|
||||
});
|
||||
|
||||
let handle = did.split(':').last().unwrap_or("user");
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"operation": {
|
||||
"type": "plc_operation",
|
||||
"rotationKeys": ["did:key:zWrongRotationKey123"],
|
||||
"verificationMethods": {"atproto": "did:key:zWrongVerificationKey456"},
|
||||
"alsoKnownAs": [format!("at://{}", handle)],
|
||||
"services": {
|
||||
"atproto_pds": {
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"endpoint": format!("https://{}", hostname)
|
||||
}
|
||||
},
|
||||
"prev": null,
|
||||
"sig": "fake_signature"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
assert!(
|
||||
body["message"].as_str().unwrap_or("").contains("signing key") ||
|
||||
body["message"].as_str().unwrap_or("").contains("rotation"),
|
||||
"Error should mention key mismatch: {:?}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_wrong_handle() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
|
||||
format!("127.0.0.1:{}", app_port())
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"operation": {
|
||||
"type": "plc_operation",
|
||||
"rotationKeys": ["did:key:z123"],
|
||||
"verificationMethods": {"atproto": "did:key:z456"},
|
||||
"alsoKnownAs": ["at://totally.wrong.handle"],
|
||||
"services": {
|
||||
"atproto_pds": {
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"endpoint": format!("https://{}", hostname)
|
||||
}
|
||||
},
|
||||
"prev": null,
|
||||
"sig": "fake_signature"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_plc_operation_wrong_service_type() {
|
||||
let client = client();
|
||||
let (token, _did) = create_account_and_login(&client).await;
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
|
||||
format!("127.0.0.1:{}", app_port())
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.json(&json!({
|
||||
"operation": {
|
||||
"type": "plc_operation",
|
||||
"rotationKeys": ["did:key:z123"],
|
||||
"verificationMethods": {"atproto": "did:key:z456"},
|
||||
"alsoKnownAs": ["at://user"],
|
||||
"services": {
|
||||
"atproto_pds": {
|
||||
"type": "WrongServiceType",
|
||||
"endpoint": format!("https://{}", hostname)
|
||||
}
|
||||
},
|
||||
"prev": null,
|
||||
"sig": "fake_signature"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: serde_json::Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_plc_token_expiry_format() {
|
||||
let client = client();
|
||||
let (token, did) = create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let db_url = get_db_connection_string().await;
|
||||
let pool = PgPool::connect(&db_url).await.expect("DB connect failed");
|
||||
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT t.expires_at
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Query failed");
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let expires = row.expires_at;
|
||||
|
||||
let diff = expires - now;
|
||||
assert!(diff.num_minutes() >= 9, "Token should expire in ~10 minutes, got {} minutes", diff.num_minutes());
|
||||
assert!(diff.num_minutes() <= 11, "Token should expire in ~10 minutes, got {} minutes", diff.num_minutes());
|
||||
}
|
||||
Reference in New Issue
Block a user