this repo has no description
1use reqwest::{header, Client, StatusCode}; 2use serde_json::{json, Value}; 3use chrono::Utc; 4#[allow(unused_imports)] 5use std::collections::HashMap; 6#[allow(unused_imports)] 7use std::time::Duration; 8use std::sync::OnceLock; 9use bspds::state::AppState; 10use sqlx::postgres::PgPoolOptions; 11use tokio::net::TcpListener; 12use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt, GenericImage}; 13use testcontainers::core::ContainerPort; 14use testcontainers_modules::postgres::Postgres; 15use aws_sdk_s3::Client as S3Client; 16use aws_config::BehaviorVersion; 17use aws_sdk_s3::config::Credentials; 18use wiremock::{MockServer, Mock, ResponseTemplate}; 19use wiremock::matchers::{method, path}; 20 21static SERVER_URL: OnceLock<String> = OnceLock::new(); 22static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new(); 23static S3_CONTAINER: OnceLock<ContainerAsync<GenericImage>> = OnceLock::new(); 24static MOCK_APPVIEW: OnceLock<MockServer> = OnceLock::new(); 25 26#[allow(dead_code)] 27pub const AUTH_TOKEN: &str = "test-token"; 28#[allow(dead_code)] 29pub const BAD_AUTH_TOKEN: &str = "bad-token"; 30#[allow(dead_code)] 31pub const AUTH_DID: &str = "did:plc:fake"; 32#[allow(dead_code)] 33pub const TARGET_DID: &str = "did:plc:target"; 34 35#[allow(dead_code)] 36pub fn client() -> Client { 37 Client::new() 38} 39 40pub async fn base_url() -> &'static str { 41 SERVER_URL.get_or_init(|| { 42 let (tx, rx) = std::sync::mpsc::channel(); 43 44 std::thread::spawn(move || { 45 if std::env::var("DOCKER_HOST").is_err() { 46 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { 47 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock"); 48 if podman_sock.exists() { 49 unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); } 50 } 51 } 52 } 53 54 let rt = tokio::runtime::Runtime::new().unwrap(); 55 rt.block_on(async move { 56 let s3_container = GenericImage::new("minio/minio", "latest") 57 .with_exposed_port(ContainerPort::Tcp(9000)) 58 .with_env_var("MINIO_ROOT_USER", "minioadmin") 59 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") 60 .with_cmd(vec!["server".to_string(), "/data".to_string()]) 61 .start() 62 .await 63 .expect("Failed to start MinIO"); 64 65 let s3_port = s3_container.get_host_port_ipv4(9000).await.expect("Failed to get S3 port"); 66 let s3_endpoint = format!("http://127.0.0.1:{}", s3_port); 67 68 unsafe { 69 std::env::set_var("S3_BUCKET", "test-bucket"); 70 std::env::set_var("AWS_ACCESS_KEY_ID", "minioadmin"); 71 std::env::set_var("AWS_SECRET_ACCESS_KEY", "minioadmin"); 72 std::env::set_var("AWS_REGION", "us-east-1"); 73 std::env::set_var("S3_ENDPOINT", &s3_endpoint); 74 } 75 76 let sdk_config = aws_config::defaults(BehaviorVersion::latest()) 77 .region("us-east-1") 78 .endpoint_url(&s3_endpoint) 79 .credentials_provider(Credentials::new("minioadmin", "minioadmin", None, None, "test")) 80 .load() 81 .await; 82 83 let s3_config = aws_sdk_s3::config::Builder::from(&sdk_config) 84 .force_path_style(true) 85 .build(); 86 let s3_client = S3Client::from_conf(s3_config); 87 88 let _ = s3_client.create_bucket().bucket("test-bucket").send().await; 89 90 let mock_server = MockServer::start().await; 91 92 Mock::given(method("GET")) 93 .and(path("/xrpc/app.bsky.actor.getProfile")) 94 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ 95 "handle": "mock.handle", 96 "did": "did:plc:mock", 97 "displayName": "Mock User" 98 }))) 99 .mount(&mock_server) 100 .await; 101 102 Mock::given(method("GET")) 103 .and(path("/xrpc/app.bsky.actor.searchActors")) 104 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ 105 "actors": [], 106 "cursor": null 107 }))) 108 .mount(&mock_server) 109 .await; 110 111 unsafe { std::env::set_var("APPVIEW_URL", mock_server.uri()); } 112 MOCK_APPVIEW.set(mock_server).ok(); 113 114 S3_CONTAINER.set(s3_container).ok(); 115 116 let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres"); 117 let connection_string = format!( 118 "postgres://postgres:postgres@127.0.0.1:{}/postgres", 119 container.get_host_port_ipv4(5432).await.expect("Failed to get port") 120 ); 121 122 DB_CONTAINER.set(container).ok(); 123 124 let url = spawn_app(connection_string).await; 125 tx.send(url).unwrap(); 126 std::future::pending::<()>().await; 127 }); 128 }); 129 130 rx.recv().expect("Failed to start test server") 131 }) 132} 133 134async fn spawn_app(database_url: String) -> String { 135 let pool = PgPoolOptions::new() 136 .connect(&database_url) 137 .await 138 .expect("Failed to connect to Postgres. Make sure the database is running."); 139 140 sqlx::migrate!("./migrations") 141 .run(&pool) 142 .await 143 .expect("Failed to run migrations"); 144 145 let state = AppState::new(pool).await; 146 let app = bspds::app(state); 147 148 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); 149 let addr = listener.local_addr().unwrap(); 150 151 tokio::spawn(async move { 152 axum::serve(listener, app).await.unwrap(); 153 }); 154 155 format!("http://{}", addr) 156} 157 158#[allow(dead_code)] 159pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { 160 let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 161 .header(header::CONTENT_TYPE, mime) 162 .bearer_auth(AUTH_TOKEN) 163 .body(data) 164 .send() 165 .await 166 .expect("Failed to send uploadBlob request"); 167 168 assert_eq!(res.status(), StatusCode::OK, "Failed to upload blob"); 169 let body: Value = res.json().await.expect("Blob upload response was not JSON"); 170 body["blob"].clone() 171} 172 173 174#[allow(dead_code)] 175pub async fn create_test_post( 176 client: &Client, 177 text: &str, 178 reply_to: Option<Value> 179) -> (String, String, String) { 180 let collection = "app.bsky.feed.post"; 181 let mut record = json!({ 182 "$type": collection, 183 "text": text, 184 "createdAt": Utc::now().to_rfc3339() 185 }); 186 187 if let Some(reply_obj) = reply_to { 188 record["reply"] = reply_obj; 189 } 190 191 let payload = json!({ 192 "repo": AUTH_DID, 193 "collection": collection, 194 "record": record 195 }); 196 197 let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 198 .bearer_auth(AUTH_TOKEN) 199 .json(&payload) 200 .send() 201 .await 202 .expect("Failed to send createRecord"); 203 204 assert_eq!(res.status(), StatusCode::OK, "Failed to create post record"); 205 let body: Value = res.json().await.expect("createRecord response was not JSON"); 206 207 let uri = body["uri"].as_str().expect("Response had no URI").to_string(); 208 let cid = body["cid"].as_str().expect("Response had no CID").to_string(); 209 let rkey = uri.split('/').last().expect("URI was malformed").to_string(); 210 211 (uri, cid, rkey) 212} 213 214#[allow(dead_code)] 215pub async fn create_account_and_login(client: &Client) -> (String, String) { 216 let handle = format!("user_{}", uuid::Uuid::new_v4()); 217 let payload = json!({ 218 "handle": handle, 219 "email": format!("{}@example.com", handle), 220 "password": "password" 221 }); 222 223 let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 224 .json(&payload) 225 .send() 226 .await 227 .expect("Failed to create account"); 228 229 if res.status() != StatusCode::OK { 230 panic!("Failed to create account: {:?}", res.text().await); 231 } 232 233 let body: Value = res.json().await.expect("Invalid JSON"); 234 let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); 235 let did = body["did"].as_str().expect("No did").to_string(); 236 (access_jwt, did) 237}