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}