this repo has no description
1use aws_config::BehaviorVersion; 2use aws_sdk_s3::Client as S3Client; 3use aws_sdk_s3::config::Credentials; 4use bspds::state::AppState; 5use chrono::Utc; 6use reqwest::{Client, StatusCode, header}; 7use serde_json::{Value, json}; 8use sqlx::postgres::PgPoolOptions; 9#[allow(unused_imports)] 10use std::collections::HashMap; 11use std::sync::OnceLock; 12#[allow(unused_imports)] 13use std::time::Duration; 14use testcontainers::core::ContainerPort; 15use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner}; 16use testcontainers_modules::postgres::Postgres; 17use tokio::net::TcpListener; 18use wiremock::matchers::{method, path}; 19use wiremock::{Mock, MockServer, ResponseTemplate}; 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 { 50 std::env::set_var( 51 "DOCKER_HOST", 52 format!("unix://{}", podman_sock.display()), 53 ); 54 } 55 } 56 } 57 } 58 59 let rt = tokio::runtime::Runtime::new().unwrap(); 60 rt.block_on(async move { 61 let s3_container = GenericImage::new("minio/minio", "latest") 62 .with_exposed_port(ContainerPort::Tcp(9000)) 63 .with_env_var("MINIO_ROOT_USER", "minioadmin") 64 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") 65 .with_cmd(vec!["server".to_string(), "/data".to_string()]) 66 .start() 67 .await 68 .expect("Failed to start MinIO"); 69 70 let s3_port = s3_container 71 .get_host_port_ipv4(9000) 72 .await 73 .expect("Failed to get S3 port"); 74 let s3_endpoint = format!("http://127.0.0.1:{}", s3_port); 75 76 unsafe { 77 std::env::set_var("S3_BUCKET", "test-bucket"); 78 std::env::set_var("AWS_ACCESS_KEY_ID", "minioadmin"); 79 std::env::set_var("AWS_SECRET_ACCESS_KEY", "minioadmin"); 80 std::env::set_var("AWS_REGION", "us-east-1"); 81 std::env::set_var("S3_ENDPOINT", &s3_endpoint); 82 } 83 84 let sdk_config = aws_config::defaults(BehaviorVersion::latest()) 85 .region("us-east-1") 86 .endpoint_url(&s3_endpoint) 87 .credentials_provider(Credentials::new( 88 "minioadmin", 89 "minioadmin", 90 None, 91 None, 92 "test", 93 )) 94 .load() 95 .await; 96 97 let s3_config = aws_sdk_s3::config::Builder::from(&sdk_config) 98 .force_path_style(true) 99 .build(); 100 let s3_client = S3Client::from_conf(s3_config); 101 102 let _ = s3_client.create_bucket().bucket("test-bucket").send().await; 103 104 let mock_server = MockServer::start().await; 105 106 Mock::given(method("GET")) 107 .and(path("/xrpc/app.bsky.actor.getProfile")) 108 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ 109 "handle": "mock.handle", 110 "did": "did:plc:mock", 111 "displayName": "Mock User" 112 }))) 113 .mount(&mock_server) 114 .await; 115 116 Mock::given(method("GET")) 117 .and(path("/xrpc/app.bsky.actor.searchActors")) 118 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ 119 "actors": [], 120 "cursor": null 121 }))) 122 .mount(&mock_server) 123 .await; 124 125 unsafe { 126 std::env::set_var("APPVIEW_URL", mock_server.uri()); 127 } 128 MOCK_APPVIEW.set(mock_server).ok(); 129 130 S3_CONTAINER.set(s3_container).ok(); 131 132 let container = Postgres::default() 133 .with_tag("18-alpine") 134 .start() 135 .await 136 .expect("Failed to start Postgres"); 137 let connection_string = format!( 138 "postgres://postgres:postgres@127.0.0.1:{}/postgres", 139 container 140 .get_host_port_ipv4(5432) 141 .await 142 .expect("Failed to get port") 143 ); 144 145 DB_CONTAINER.set(container).ok(); 146 147 let url = spawn_app(connection_string).await; 148 tx.send(url).unwrap(); 149 std::future::pending::<()>().await; 150 }); 151 }); 152 153 rx.recv().expect("Failed to start test server") 154 }) 155} 156 157async fn spawn_app(database_url: String) -> String { 158 let pool = PgPoolOptions::new() 159 .max_connections(50) 160 .connect(&database_url) 161 .await 162 .expect("Failed to connect to Postgres. Make sure the database is running."); 163 164 sqlx::migrate!("./migrations") 165 .run(&pool) 166 .await 167 .expect("Failed to run migrations"); 168 169 let state = AppState::new(pool).await; 170 let app = bspds::app(state); 171 172 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); 173 let addr = listener.local_addr().unwrap(); 174 175 tokio::spawn(async move { 176 axum::serve(listener, app).await.unwrap(); 177 }); 178 179 format!("http://{}", addr) 180} 181 182#[allow(dead_code)] 183pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { 184 let res = client 185 .post(format!( 186 "{}/xrpc/com.atproto.repo.uploadBlob", 187 base_url().await 188 )) 189 .header(header::CONTENT_TYPE, mime) 190 .bearer_auth(AUTH_TOKEN) 191 .body(data) 192 .send() 193 .await 194 .expect("Failed to send uploadBlob request"); 195 196 assert_eq!(res.status(), StatusCode::OK, "Failed to upload blob"); 197 let body: Value = res.json().await.expect("Blob upload response was not JSON"); 198 body["blob"].clone() 199} 200 201#[allow(dead_code)] 202pub async fn create_test_post( 203 client: &Client, 204 text: &str, 205 reply_to: Option<Value>, 206) -> (String, String, String) { 207 let collection = "app.bsky.feed.post"; 208 let mut record = json!({ 209 "$type": collection, 210 "text": text, 211 "createdAt": Utc::now().to_rfc3339() 212 }); 213 214 if let Some(reply_obj) = reply_to { 215 record["reply"] = reply_obj; 216 } 217 218 let payload = json!({ 219 "repo": AUTH_DID, 220 "collection": collection, 221 "record": record 222 }); 223 224 let res = client 225 .post(format!( 226 "{}/xrpc/com.atproto.repo.createRecord", 227 base_url().await 228 )) 229 .bearer_auth(AUTH_TOKEN) 230 .json(&payload) 231 .send() 232 .await 233 .expect("Failed to send createRecord"); 234 235 assert_eq!(res.status(), StatusCode::OK, "Failed to create post record"); 236 let body: Value = res 237 .json() 238 .await 239 .expect("createRecord response was not JSON"); 240 241 let uri = body["uri"] 242 .as_str() 243 .expect("Response had no URI") 244 .to_string(); 245 let cid = body["cid"] 246 .as_str() 247 .expect("Response had no CID") 248 .to_string(); 249 let rkey = uri 250 .split('/') 251 .last() 252 .expect("URI was malformed") 253 .to_string(); 254 255 (uri, cid, rkey) 256} 257 258#[allow(dead_code)] 259pub async fn create_account_and_login(client: &Client) -> (String, String) { 260 let mut last_error = String::new(); 261 262 for attempt in 0..3 { 263 if attempt > 0 { 264 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 265 } 266 267 let handle = format!("user_{}", uuid::Uuid::new_v4()); 268 let payload = json!({ 269 "handle": handle, 270 "email": format!("{}@example.com", handle), 271 "password": "password" 272 }); 273 274 let res = match client 275 .post(format!( 276 "{}/xrpc/com.atproto.server.createAccount", 277 base_url().await 278 )) 279 .json(&payload) 280 .send() 281 .await 282 { 283 Ok(r) => r, 284 Err(e) => { 285 last_error = format!("Request failed: {}", e); 286 continue; 287 } 288 }; 289 290 if res.status() == StatusCode::OK { 291 let body: Value = res.json().await.expect("Invalid JSON"); 292 let access_jwt = body["accessJwt"] 293 .as_str() 294 .expect("No accessJwt") 295 .to_string(); 296 let did = body["did"].as_str().expect("No did").to_string(); 297 return (access_jwt, did); 298 } 299 300 last_error = format!("Status {}: {:?}", res.status(), res.text().await); 301 } 302 303 panic!("Failed to create account after 3 attempts: {}", last_error); 304}