Format and split big files into smaller ones

This commit is contained in:
Lewis
2025-12-07 11:47:38 +02:00
parent 7b90694066
commit e2cc51f0b1
34 changed files with 3067 additions and 1776 deletions
+7 -6
View File
@@ -1,10 +1,10 @@
use bspds::auth;
use k256::SecretKey;
use rand::rngs::OsRng;
use chrono::{Utc, Duration};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde_json::json;
use bspds::auth;
use chrono::{Duration, Utc};
use k256::SecretKey;
use k256::ecdsa::{SigningKey, signature::Signer};
use rand::rngs::OsRng;
use serde_json::json;
#[test]
fn test_jwt_flow() {
@@ -24,7 +24,8 @@ fn test_jwt_flow() {
let aud = "did:web:service";
let lxm = "com.example.test";
let s_token = auth::create_service_token(did, aud, lxm, &key_bytes).expect("create service token");
let s_token =
auth::create_service_token(did, aud, lxm, &key_bytes).expect("create service token");
let s_data = auth::verify_token(&s_token, &key_bytes).expect("verify service token");
assert_eq!(s_data.claims.aud, aud);
assert_eq!(s_data.claims.lxm, Some(lxm.to_string()));
+77 -27
View File
@@ -1,22 +1,22 @@
use reqwest::{header, Client, StatusCode};
use serde_json::{json, Value};
use aws_config::BehaviorVersion;
use aws_sdk_s3::Client as S3Client;
use aws_sdk_s3::config::Credentials;
use bspds::state::AppState;
use chrono::Utc;
use reqwest::{Client, StatusCode, header};
use serde_json::{Value, json};
use sqlx::postgres::PgPoolOptions;
#[allow(unused_imports)]
use std::collections::HashMap;
use std::sync::OnceLock;
#[allow(unused_imports)]
use std::time::Duration;
use std::sync::OnceLock;
use bspds::state::AppState;
use sqlx::postgres::PgPoolOptions;
use tokio::net::TcpListener;
use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt, GenericImage};
use testcontainers::core::ContainerPort;
use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner};
use testcontainers_modules::postgres::Postgres;
use aws_sdk_s3::Client as S3Client;
use aws_config::BehaviorVersion;
use aws_sdk_s3::config::Credentials;
use wiremock::{MockServer, Mock, ResponseTemplate};
use tokio::net::TcpListener;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
static SERVER_URL: OnceLock<String> = OnceLock::new();
static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
@@ -46,7 +46,12 @@ pub async fn base_url() -> &'static str {
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
if podman_sock.exists() {
unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); }
unsafe {
std::env::set_var(
"DOCKER_HOST",
format!("unix://{}", podman_sock.display()),
);
}
}
}
}
@@ -62,7 +67,10 @@ pub async fn base_url() -> &'static str {
.await
.expect("Failed to start MinIO");
let s3_port = s3_container.get_host_port_ipv4(9000).await.expect("Failed to get S3 port");
let s3_port = s3_container
.get_host_port_ipv4(9000)
.await
.expect("Failed to get S3 port");
let s3_endpoint = format!("http://127.0.0.1:{}", s3_port);
unsafe {
@@ -76,7 +84,13 @@ pub async fn base_url() -> &'static str {
let sdk_config = aws_config::defaults(BehaviorVersion::latest())
.region("us-east-1")
.endpoint_url(&s3_endpoint)
.credentials_provider(Credentials::new("minioadmin", "minioadmin", None, None, "test"))
.credentials_provider(Credentials::new(
"minioadmin",
"minioadmin",
None,
None,
"test",
))
.load()
.await;
@@ -108,15 +122,24 @@ pub async fn base_url() -> &'static str {
.mount(&mock_server)
.await;
unsafe { std::env::set_var("APPVIEW_URL", mock_server.uri()); }
unsafe {
std::env::set_var("APPVIEW_URL", mock_server.uri());
}
MOCK_APPVIEW.set(mock_server).ok();
S3_CONTAINER.set(s3_container).ok();
let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres");
let container = Postgres::default()
.with_tag("18-alpine")
.start()
.await
.expect("Failed to start Postgres");
let connection_string = format!(
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
container.get_host_port_ipv4(5432).await.expect("Failed to get port")
container
.get_host_port_ipv4(5432)
.await
.expect("Failed to get port")
);
DB_CONTAINER.set(container).ok();
@@ -157,7 +180,11 @@ async fn spawn_app(database_url: String) -> String {
#[allow(dead_code)]
pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.uploadBlob",
base_url().await
))
.header(header::CONTENT_TYPE, mime)
.bearer_auth(AUTH_TOKEN)
.body(data)
@@ -170,12 +197,11 @@ pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'stati
body["blob"].clone()
}
#[allow(dead_code)]
pub async fn create_test_post(
client: &Client,
text: &str,
reply_to: Option<Value>
reply_to: Option<Value>,
) -> (String, String, String) {
let collection = "app.bsky.feed.post";
let mut record = json!({
@@ -194,7 +220,11 @@ pub async fn create_test_post(
"record": record
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.createRecord",
base_url().await
))
.bearer_auth(AUTH_TOKEN)
.json(&payload)
.send()
@@ -202,11 +232,24 @@ pub async fn create_test_post(
.expect("Failed to send createRecord");
assert_eq!(res.status(), StatusCode::OK, "Failed to create post record");
let body: Value = res.json().await.expect("createRecord response was not JSON");
let body: Value = res
.json()
.await
.expect("createRecord response was not JSON");
let uri = body["uri"].as_str().expect("Response had no URI").to_string();
let cid = body["cid"].as_str().expect("Response had no CID").to_string();
let rkey = uri.split('/').last().expect("URI was malformed").to_string();
let uri = body["uri"]
.as_str()
.expect("Response had no URI")
.to_string();
let cid = body["cid"]
.as_str()
.expect("Response had no CID")
.to_string();
let rkey = uri
.split('/')
.last()
.expect("URI was malformed")
.to_string();
(uri, cid, rkey)
}
@@ -220,7 +263,11 @@ pub async fn create_account_and_login(client: &Client) -> (String, String) {
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await
@@ -231,7 +278,10 @@ pub async fn create_account_and_login(client: &Client) -> (String, String) {
}
let body: Value = res.json().await.expect("Invalid JSON");
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
let access_jwt = body["accessJwt"]
.as_str()
.expect("No accessJwt")
.to_string();
let did = body["did"].as_str().expect("No did").to_string();
(access_jwt, did)
}
+39 -11
View File
@@ -1,9 +1,9 @@
mod common;
use common::*;
use reqwest::StatusCode;
use serde_json::{json, Value};
use wiremock::{MockServer, Mock, ResponseTemplate};
use serde_json::{Value, json};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
// #[tokio::test]
// async fn test_resolve_handle() {
@@ -23,7 +23,8 @@ use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_well_known_did() {
let client = client();
let res = client.get(format!("{}/.well-known/did.json", base_url().await))
let res = client
.get(format!("{}/.well-known/did.json", base_url().await))
.send()
.await
.expect("Failed to send request");
@@ -71,7 +72,11 @@ async fn test_create_did_web_account_and_resolve() {
"did": did
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await
@@ -79,13 +84,20 @@ async fn test_create_did_web_account_and_resolve() {
if res.status() != StatusCode::OK {
let status = res.status();
let body: Value = res.json().await.unwrap_or(json!({"error": "could not parse body"}));
let body: Value = res
.json()
.await
.unwrap_or(json!({"error": "could not parse body"}));
panic!("createAccount failed with status {}: {:?}", status, body);
}
let body: Value = res.json().await.expect("createAccount response was not JSON");
let body: Value = res
.json()
.await
.expect("createAccount response was not JSON");
assert_eq!(body["did"], did);
let res = client.get(format!("{}/u/{}/did.json", base_url().await, handle))
let res = client
.get(format!("{}/u/{}/did.json", base_url().await, handle))
.send()
.await
.expect("Failed to fetch DID doc");
@@ -111,14 +123,22 @@ async fn test_create_account_duplicate_handle() {
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(res.status(), StatusCode::OK);
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await
@@ -143,7 +163,11 @@ async fn test_did_web_lifecycle() {
"did": did
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_payload)
.send()
.await
@@ -162,7 +186,11 @@ async fn test_did_web_lifecycle() {
"identifier": handle,
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createSession",
base_url().await
))
.json(&login_payload)
.send()
.await
+376 -45
View File
@@ -1,10 +1,9 @@
mod common;
use common::*;
use reqwest::{Client, StatusCode};
use serde_json::{json, Value};
use chrono::Utc;
#[allow(unused_imports)]
use reqwest;
use serde_json::{Value, json};
use std::time::Duration;
async fn setup_new_user(handle_prefix: &str) -> (String, String) {
@@ -19,20 +18,36 @@ async fn setup_new_user(handle_prefix: &str) -> (String, String) {
"email": email,
"password": password
});
let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_account_payload)
.send()
.await
.expect("setup_new_user: Failed to send createAccount");
if create_res.status() != StatusCode::OK {
panic!("setup_new_user: Failed to create account: {:?}", create_res.text().await);
if create_res.status() != reqwest::StatusCode::OK {
panic!(
"setup_new_user: Failed to create account: {:?}",
create_res.text().await
);
}
let create_body: Value = create_res.json().await.expect("setup_new_user: createAccount response was not JSON");
let create_body: Value = create_res
.json()
.await
.expect("setup_new_user: createAccount response was not JSON");
let new_did = create_body["did"].as_str().expect("setup_new_user: Response had no DID").to_string();
let new_jwt = create_body["accessJwt"].as_str().expect("setup_new_user: Response had no accessJwt").to_string();
let new_did = create_body["did"]
.as_str()
.expect("setup_new_user: Response had no DID")
.to_string();
let new_jwt = create_body["accessJwt"]
.as_str()
.expect("setup_new_user: Response had no accessJwt")
.to_string();
(new_did, new_jwt)
}
@@ -59,35 +74,59 @@ async fn test_post_crud_lifecycle() {
}
});
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&jwt)
.json(&create_payload)
.send()
.await
.expect("Failed to send create request");
assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record");
let create_body: Value = create_res.json().await.expect("create response was not JSON");
let uri = create_body["uri"].as_str().unwrap();
if create_res.status() != reqwest::StatusCode::OK {
let status = create_res.status();
let body = create_res
.text()
.await
.unwrap_or_else(|_| "Could not get body".to_string());
panic!(
"Failed to create record. Status: {}, Body: {}",
status, body
);
}
let create_body: Value = create_res
.json()
.await
.expect("create response was not JSON");
let uri = create_body["uri"].as_str().unwrap();
let params = [
("repo", did.as_str()),
("collection", collection),
("rkey", &rkey),
];
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let get_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&params)
.send()
.await
.expect("Failed to send get request");
assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create");
assert_eq!(
get_res.status(),
reqwest::StatusCode::OK,
"Failed to get record after create"
);
let get_body: Value = get_res.json().await.expect("get response was not JSON");
assert_eq!(get_body["uri"], uri);
assert_eq!(get_body["value"]["text"], original_text);
let updated_text = "This post has been updated.";
let update_payload = json!({
"repo": did,
@@ -100,26 +139,46 @@ async fn test_post_crud_lifecycle() {
}
});
let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let update_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&jwt)
.json(&update_payload)
.send()
.await
.expect("Failed to send update request");
assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
assert_eq!(
update_res.status(),
reqwest::StatusCode::OK,
"Failed to update record"
);
let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let get_updated_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&params)
.send()
.await
.expect("Failed to send get-after-update request");
assert_eq!(get_updated_res.status(), StatusCode::OK, "Failed to get record after update");
let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON");
assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated");
assert_eq!(
get_updated_res.status(),
reqwest::StatusCode::OK,
"Failed to get record after update"
);
let get_updated_body: Value = get_updated_res
.json()
.await
.expect("get-updated response was not JSON");
assert_eq!(
get_updated_body["value"]["text"], updated_text,
"Text was not updated"
);
let delete_payload = json!({
"repo": did,
@@ -127,23 +186,38 @@ async fn test_post_crud_lifecycle() {
"rkey": rkey
});
let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.deleteRecord",
base_url().await
))
.bearer_auth(&jwt)
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
assert_eq!(
delete_res.status(),
reqwest::StatusCode::OK,
"Failed to delete record"
);
let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let get_deleted_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&params)
.send()
.await
.expect("Failed to send get-after-delete request");
assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record was found, but it should be deleted");
assert_eq!(
get_deleted_res.status(),
reqwest::StatusCode::NOT_FOUND,
"Record was found, but it should be deleted"
);
}
#[tokio::test]
@@ -161,24 +235,39 @@ async fn test_record_update_conflict_lifecycle() {
"displayName": "Original Name"
}
});
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&user_jwt)
.json(&profile_payload)
.send().await.expect("create profile failed");
.send()
.await
.expect("create profile failed");
if create_res.status() != StatusCode::OK {
if create_res.status() != reqwest::StatusCode::OK {
return;
}
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let get_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&[
("repo", &user_did),
("collection", &"app.bsky.actor.profile".to_string()),
("rkey", &"self".to_string()),
])
.send().await.expect("getRecord failed");
.send()
.await
.expect("getRecord failed");
let get_body: Value = get_res.json().await.expect("getRecord not json");
let cid_v1 = get_body["cid"].as_str().expect("Profile v1 had no CID").to_string();
let cid_v1 = get_body["cid"]
.as_str()
.expect("Profile v1 had no CID")
.to_string();
let update_payload_v2 = json!({
"repo": user_did,
@@ -190,13 +279,26 @@ async fn test_record_update_conflict_lifecycle() {
},
"swapCommit": cid_v1 // <-- Correctly point to v1
});
let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let update_res_v2 = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&user_jwt)
.json(&update_payload_v2)
.send().await.expect("putRecord v2 failed");
assert_eq!(update_res_v2.status(), StatusCode::OK, "v2 update failed");
.send()
.await
.expect("putRecord v2 failed");
assert_eq!(
update_res_v2.status(),
reqwest::StatusCode::OK,
"v2 update failed"
);
let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
let cid_v2 = update_body_v2["cid"].as_str().expect("v2 response had no CID").to_string();
let cid_v2 = update_body_v2["cid"]
.as_str()
.expect("v2 response had no CID")
.to_string();
let update_payload_v3_stale = json!({
"repo": user_did,
@@ -208,14 +310,20 @@ async fn test_record_update_conflict_lifecycle() {
},
"swapCommit": cid_v1
});
let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let update_res_v3_stale = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&user_jwt)
.json(&update_payload_v3_stale)
.send().await.expect("putRecord v3 (stale) failed");
.send()
.await
.expect("putRecord v3 (stale) failed");
assert_eq!(
update_res_v3_stale.status(),
StatusCode::CONFLICT,
reqwest::StatusCode::CONFLICT,
"Stale update did not cause a 409 Conflict"
);
@@ -229,10 +337,233 @@ async fn test_record_update_conflict_lifecycle() {
},
"swapCommit": cid_v2 // <-- Correct
});
let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let update_res_v3_good = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&user_jwt)
.json(&update_payload_v3_good)
.send().await.expect("putRecord v3 (good) failed");
.send()
.await
.expect("putRecord v3 (good) failed");
assert_eq!(update_res_v3_good.status(), StatusCode::OK, "v3 (good) update failed");
assert_eq!(
update_res_v3_good.status(),
reqwest::StatusCode::OK,
"v3 (good) update failed"
);
}
async fn create_post(
client: &reqwest::Client,
did: &str,
jwt: &str,
text: &str,
) -> (String, String) {
let collection = "app.bsky.feed.post";
let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
let now = Utc::now().to_rfc3339();
let create_payload = json!({
"repo": did,
"collection": collection,
"rkey": rkey,
"record": {
"$type": collection,
"text": text,
"createdAt": now
}
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(jwt)
.json(&create_payload)
.send()
.await
.expect("Failed to send create post request");
assert_eq!(
create_res.status(),
reqwest::StatusCode::OK,
"Failed to create post record"
);
let create_body: Value = create_res
.json()
.await
.expect("create post response was not JSON");
let uri = create_body["uri"].as_str().unwrap().to_string();
let cid = create_body["cid"].as_str().unwrap().to_string();
(uri, cid)
}
async fn create_follow(
client: &reqwest::Client,
follower_did: &str,
follower_jwt: &str,
followee_did: &str,
) -> (String, String) {
let collection = "app.bsky.graph.follow";
let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
let now = Utc::now().to_rfc3339();
let create_payload = json!({
"repo": follower_did,
"collection": collection,
"rkey": rkey,
"record": {
"$type": collection,
"subject": followee_did,
"createdAt": now
}
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(follower_jwt)
.json(&create_payload)
.send()
.await
.expect("Failed to send create follow request");
assert_eq!(
create_res.status(),
reqwest::StatusCode::OK,
"Failed to create follow record"
);
let create_body: Value = create_res
.json()
.await
.expect("create follow response was not JSON");
let uri = create_body["uri"].as_str().unwrap().to_string();
let cid = create_body["cid"].as_str().unwrap().to_string();
(uri, cid)
}
#[tokio::test]
#[ignore]
async fn test_social_flow_lifecycle() {
let client = client();
let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
tokio::time::sleep(Duration::from_secs(1)).await;
let timeline_res_1 = client
.get(format!(
"{}/xrpc/app.bsky.feed.getTimeline",
base_url().await
))
.bearer_auth(&bob_jwt)
.send()
.await
.expect("Failed to get timeline (1)");
assert_eq!(
timeline_res_1.status(),
reqwest::StatusCode::OK,
"Failed to get timeline (1)"
);
let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
let feed_1 = timeline_body_1["feed"].as_array().unwrap();
assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
assert_eq!(
feed_1[0]["post"]["uri"], post1_uri,
"Post URI mismatch in timeline (1)"
);
let (post2_uri, _) = create_post(
&client,
&alice_did,
&alice_jwt,
"Alice's second post, so exciting!",
)
.await;
tokio::time::sleep(Duration::from_secs(1)).await;
let timeline_res_2 = client
.get(format!(
"{}/xrpc/app.bsky.feed.getTimeline",
base_url().await
))
.bearer_auth(&bob_jwt)
.send()
.await
.expect("Failed to get timeline (2)");
assert_eq!(
timeline_res_2.status(),
reqwest::StatusCode::OK,
"Failed to get timeline (2)"
);
let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
let feed_2 = timeline_body_2["feed"].as_array().unwrap();
assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
assert_eq!(
feed_2[0]["post"]["uri"], post2_uri,
"Post 2 should be first"
);
assert_eq!(
feed_2[1]["post"]["uri"], post1_uri,
"Post 1 should be second"
);
let delete_payload = json!({
"repo": alice_did,
"collection": "app.bsky.feed.post",
"rkey": post1_uri.split('/').last().unwrap()
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.deleteRecord",
base_url().await
))
.bearer_auth(&alice_jwt)
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(
delete_res.status(),
reqwest::StatusCode::OK,
"Failed to delete record"
);
tokio::time::sleep(Duration::from_secs(1)).await;
let timeline_res_3 = client
.get(format!(
"{}/xrpc/app.bsky.feed.getTimeline",
base_url().await
))
.bearer_auth(&bob_jwt)
.send()
.await
.expect("Failed to get timeline (3)");
assert_eq!(
timeline_res_3.status(),
reqwest::StatusCode::OK,
"Failed to get timeline (3)"
);
let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
let feed_3 = timeline_body_3["feed"].as_array().unwrap();
assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
assert_eq!(
feed_3[0]["post"]["uri"], post2_uri,
"Only post 2 should remain"
);
}
+24 -16
View File
@@ -1,17 +1,15 @@
mod common;
use axum::{
routing::any,
Router,
extract::Request,
http::StatusCode,
};
use tokio::net::TcpListener;
use axum::{Router, extract::Request, http::StatusCode, routing::any};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use reqwest::Client;
use std::sync::Arc;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use tokio::net::TcpListener;
async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option<String>)>) {
async fn spawn_mock_upstream() -> (
String,
tokio::sync::mpsc::Receiver<(String, String, Option<String>)>,
) {
let (tx, rx) = tokio::sync::mpsc::channel(10);
let tx = Arc::new(tx);
@@ -20,7 +18,9 @@ async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String,
async move {
let method = req.method().to_string();
let uri = req.uri().to_string();
let auth = req.headers().get("Authorization")
let auth = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
@@ -45,7 +45,8 @@ async fn test_proxy_via_header() {
let (upstream_url, mut rx) = spawn_mock_upstream().await;
let client = Client::new();
let res = client.get(format!("{}/xrpc/com.example.test", app_url))
let res = client
.get(format!("{}/xrpc/com.example.test", app_url))
.header("atproto-proxy", &upstream_url)
.header("Authorization", "Bearer test-token")
.send()
@@ -65,12 +66,15 @@ async fn test_proxy_via_header() {
async fn test_proxy_via_env_var() {
let (upstream_url, mut rx) = spawn_mock_upstream().await;
unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); }
unsafe {
std::env::set_var("APPVIEW_URL", &upstream_url);
}
let app_url = common::base_url().await;
let client = Client::new();
let res = client.get(format!("{}/xrpc/com.example.envtest", app_url))
let res = client
.get(format!("{}/xrpc/com.example.envtest", app_url))
.send()
.await
.unwrap();
@@ -85,12 +89,15 @@ async fn test_proxy_via_env_var() {
#[tokio::test]
#[ignore]
async fn test_proxy_missing_config() {
unsafe { std::env::remove_var("APPVIEW_URL"); }
unsafe {
std::env::remove_var("APPVIEW_URL");
}
let app_url = common::base_url().await;
let client = Client::new();
let res = client.get(format!("{}/xrpc/com.example.fail", app_url))
let res = client
.get(format!("{}/xrpc/com.example.fail", app_url))
.send()
.await
.unwrap();
@@ -106,7 +113,8 @@ async fn test_proxy_auth_signing() {
let (access_jwt, did) = common::create_account_and_login(&client).await;
let res = client.get(format!("{}/xrpc/com.example.signed", app_url))
let res = client
.get(format!("{}/xrpc/com.example.signed", app_url))
.header("atproto-proxy", &upstream_url)
.header("Authorization", format!("Bearer {}", access_jwt))
.send()
+103 -28
View File
@@ -1,9 +1,9 @@
mod common;
use common::*;
use reqwest::{header, StatusCode};
use serde_json::{json, Value};
use chrono::Utc;
use reqwest::{StatusCode, header};
use serde_json::{Value, json};
#[tokio::test]
#[ignore]
@@ -15,7 +15,11 @@ async fn test_get_record() {
("rkey", "self"),
];
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&params)
.send()
.await
@@ -36,7 +40,11 @@ async fn test_get_record_not_found() {
("rkey", "nonexistent"),
];
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&params)
.send()
.await
@@ -50,7 +58,11 @@ async fn test_get_record_not_found() {
#[tokio::test]
async fn test_upload_blob_no_auth() {
let client = client();
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.uploadBlob",
base_url().await
))
.header(header::CONTENT_TYPE, "text/plain")
.body("no auth")
.send()
@@ -66,7 +78,11 @@ async fn test_upload_blob_no_auth() {
async fn test_upload_blob_success() {
let client = client();
let (token, _) = create_account_and_login(&client).await;
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.uploadBlob",
base_url().await
))
.header(header::CONTENT_TYPE, "text/plain")
.bearer_auth(token)
.body("This is our blob data")
@@ -90,7 +106,11 @@ async fn test_put_record_no_auth() {
"record": {}
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.json(&payload)
.send()
.await
@@ -118,7 +138,11 @@ async fn test_put_record_success() {
}
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(token)
.json(&payload)
.send()
@@ -135,23 +159,33 @@ async fn test_put_record_success() {
#[ignore]
async fn test_get_record_missing_params() {
let client = client();
let params = [
("repo", "did:plc:12345"),
];
let params = [("repo", "did:plc:12345")];
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&params)
.send()
.await
.expect("Failed to send request");
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for missing params");
assert_eq!(
res.status(),
StatusCode::BAD_REQUEST,
"Expected 400 for missing params"
);
}
#[tokio::test]
async fn test_upload_blob_bad_token() {
let client = client();
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.uploadBlob",
base_url().await
))
.header(header::CONTENT_TYPE, "text/plain")
.bearer_auth(BAD_AUTH_TOKEN)
.body("This is our blob data")
@@ -181,14 +215,22 @@ async fn test_put_record_mismatched_repo() {
}
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(token)
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(res.status(), StatusCode::FORBIDDEN, "Expected 403 for mismatched repo and auth");
assert_eq!(
res.status(),
StatusCode::FORBIDDEN,
"Expected 403 for mismatched repo and auth"
);
}
#[tokio::test]
@@ -207,21 +249,33 @@ async fn test_put_record_invalid_schema() {
}
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(token)
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid record schema");
assert_eq!(
res.status(),
StatusCode::BAD_REQUEST,
"Expected 400 for invalid record schema"
);
}
#[tokio::test]
async fn test_upload_blob_unsupported_mime_type() {
let client = client();
let (token, _) = create_account_and_login(&client).await;
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.uploadBlob",
base_url().await
))
.header(header::CONTENT_TYPE, "application/xml")
.bearer_auth(token)
.body("<xml>not an image</xml>")
@@ -242,7 +296,11 @@ async fn test_list_records() {
("collection", "app.bsky.feed.post"),
("limit", "10"),
];
let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.repo.listRecords",
base_url().await
))
.query(&params)
.send()
.await
@@ -255,10 +313,12 @@ async fn test_list_records() {
async fn test_describe_repo() {
let client = client();
let (_, did) = create_account_and_login(&client).await;
let params = [
("repo", did.as_str()),
];
let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await))
let params = [("repo", did.as_str())];
let res = client
.get(format!(
"{}/xrpc/com.atproto.repo.describeRepo",
base_url().await
))
.query(&params)
.send()
.await
@@ -282,7 +342,11 @@ async fn test_create_record_success_with_generated_rkey() {
}
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.createRecord",
base_url().await
))
.json(&payload)
.bearer_auth(token)
.send()
@@ -313,7 +377,11 @@ async fn test_create_record_success_with_provided_rkey() {
}
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.createRecord",
base_url().await
))
.json(&payload)
.bearer_auth(token)
.send()
@@ -322,7 +390,10 @@ async fn test_create_record_success_with_provided_rkey() {
assert_eq!(res.status(), StatusCode::OK);
let body: Value = res.json().await.expect("Response was not valid JSON");
assert_eq!(body["uri"], format!("at://{}/app.bsky.feed.post/{}", did, rkey));
assert_eq!(
body["uri"],
format!("at://{}/app.bsky.feed.post/{}", did, rkey)
);
// assert_eq!(body["cid"], "bafyreihy");
}
@@ -336,7 +407,11 @@ async fn test_delete_record() {
"collection": "app.bsky.feed.post",
"rkey": "some_post_to_delete"
});
let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.repo.deleteRecord",
base_url().await
))
.bearer_auth(token)
.json(&payload)
.send()
+71 -17
View File
@@ -2,12 +2,13 @@ mod common;
use common::*;
use reqwest::StatusCode;
use serde_json::{json, Value};
use serde_json::{Value, json};
#[tokio::test]
async fn test_health() {
let client = client();
let res = client.get(format!("{}/health", base_url().await))
let res = client
.get(format!("{}/health", base_url().await))
.send()
.await
.expect("Failed to send request");
@@ -19,7 +20,11 @@ async fn test_health() {
#[tokio::test]
async fn test_describe_server() {
let client = client();
let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.server.describeServer",
base_url().await
))
.send()
.await
.expect("Failed to send request");
@@ -39,7 +44,11 @@ async fn test_create_session() {
"email": format!("{}@example.com", handle),
"password": "password"
});
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let _ = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await;
@@ -49,7 +58,11 @@ async fn test_create_session() {
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createSession",
base_url().await
))
.json(&payload)
.send()
.await
@@ -67,14 +80,21 @@ async fn test_create_session_missing_identifier() {
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createSession",
base_url().await
))
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert!(res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY,
"Expected 400 or 422 for missing identifier, got {}", res.status());
assert!(
res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY,
"Expected 400 or 422 for missing identifier, got {}",
res.status()
);
}
#[tokio::test]
@@ -86,19 +106,31 @@ async fn test_create_account_invalid_handle() {
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid handle chars");
assert_eq!(
res.status(),
StatusCode::BAD_REQUEST,
"Expected 400 for invalid handle chars"
);
}
#[tokio::test]
async fn test_get_session() {
let client = client();
let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.server.getSession",
base_url().await
))
.bearer_auth(AUTH_TOKEN)
.send()
.await
@@ -117,7 +149,11 @@ async fn test_refresh_session() {
"email": format!("{}@example.com", handle),
"password": "password"
});
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
let _ = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&payload)
.send()
.await;
@@ -126,7 +162,11 @@ async fn test_refresh_session() {
"identifier": handle,
"password": "password"
});
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.createSession",
base_url().await
))
.json(&login_payload)
.send()
.await
@@ -134,10 +174,20 @@ async fn test_refresh_session() {
assert_eq!(res.status(), StatusCode::OK);
let body: Value = res.json().await.expect("Invalid JSON");
let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string();
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
let refresh_jwt = body["refreshJwt"]
.as_str()
.expect("No refreshJwt")
.to_string();
let access_jwt = body["accessJwt"]
.as_str()
.expect("No accessJwt")
.to_string();
let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.refreshSession",
base_url().await
))
.bearer_auth(&refresh_jwt)
.send()
.await
@@ -154,7 +204,11 @@ async fn test_refresh_session() {
#[tokio::test]
async fn test_delete_session() {
let client = client();
let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await))
let res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteSession",
base_url().await
))
.bearer_auth(AUTH_TOKEN)
.send()
.await
+11 -5
View File
@@ -6,10 +6,12 @@ use reqwest::StatusCode;
#[ignore]
async fn test_get_repo() {
let client = client();
let params = [
("did", AUTH_DID),
];
let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await))
let params = [("did", AUTH_DID)];
let res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo",
base_url().await
))
.query(&params)
.send()
.await
@@ -26,7 +28,11 @@ async fn test_get_blocks() {
("did", AUTH_DID),
// "cids" would be a list of CIDs
];
let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await))
let res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getBlocks",
base_url().await
))
.query(&params)
.send()
.await