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}