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};
13use testcontainers_modules::postgres::Postgres;
14
15static SERVER_URL: OnceLock<String> = OnceLock::new();
16static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
17
18#[allow(dead_code)]
19pub const AUTH_TOKEN: &str = "test-token";
20#[allow(dead_code)]
21pub const BAD_AUTH_TOKEN: &str = "bad-token";
22#[allow(dead_code)]
23pub const AUTH_DID: &str = "did:plc:fake";
24#[allow(dead_code)]
25pub const TARGET_DID: &str = "did:plc:target";
26
27pub fn client() -> Client {
28 Client::new()
29}
30
31pub async fn base_url() -> &'static str {
32 SERVER_URL.get_or_init(|| {
33 let (tx, rx) = std::sync::mpsc::channel();
34
35 std::thread::spawn(move || {
36 if std::env::var("DOCKER_HOST").is_err() {
37 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
38 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
39 if podman_sock.exists() {
40 unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); }
41 }
42 }
43 }
44
45 let rt = tokio::runtime::Runtime::new().unwrap();
46 rt.block_on(async move {
47 let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres");
48 let connection_string = format!(
49 "postgres://postgres:postgres@127.0.0.1:{}/postgres",
50 container.get_host_port_ipv4(5432).await.expect("Failed to get port")
51 );
52
53 DB_CONTAINER.set(container).ok();
54
55 let url = spawn_app(connection_string).await;
56 tx.send(url).unwrap();
57 std::future::pending::<()>().await;
58 });
59 });
60
61 rx.recv().expect("Failed to start test server")
62 })
63}
64
65async fn spawn_app(database_url: String) -> String {
66 let pool = PgPoolOptions::new()
67 .connect(&database_url)
68 .await
69 .expect("Failed to connect to Postgres. Make sure the database is running.");
70
71 sqlx::migrate!("./migrations")
72 .run(&pool)
73 .await
74 .expect("Failed to run migrations");
75
76 let state = AppState::new(pool);
77 let app = bspds::app(state);
78
79 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
80 let addr = listener.local_addr().unwrap();
81
82 tokio::spawn(async move {
83 axum::serve(listener, app).await.unwrap();
84 });
85
86 format!("http://{}", addr)
87}
88
89#[allow(dead_code)]
90pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
91 let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
92 .header(header::CONTENT_TYPE, mime)
93 .bearer_auth(AUTH_TOKEN)
94 .body(data)
95 .send()
96 .await
97 .expect("Failed to send uploadBlob request");
98
99 assert_eq!(res.status(), StatusCode::OK, "Failed to upload blob");
100 let body: Value = res.json().await.expect("Blob upload response was not JSON");
101 body["blob"].clone()
102}
103
104
105#[allow(dead_code)]
106pub async fn create_test_post(
107 client: &Client,
108 text: &str,
109 reply_to: Option<Value>
110) -> (String, String, String) {
111 let collection = "app.bsky.feed.post";
112 let mut record = json!({
113 "$type": collection,
114 "text": text,
115 "createdAt": Utc::now().to_rfc3339()
116 });
117
118 if let Some(reply_obj) = reply_to {
119 record["reply"] = reply_obj;
120 }
121
122 let payload = json!({
123 "repo": AUTH_DID,
124 "collection": collection,
125 "record": record
126 });
127
128 let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
129 .bearer_auth(AUTH_TOKEN)
130 .json(&payload)
131 .send()
132 .await
133 .expect("Failed to send createRecord");
134
135 assert_eq!(res.status(), StatusCode::OK, "Failed to create post record");
136 let body: Value = res.json().await.expect("createRecord response was not JSON");
137
138 let uri = body["uri"].as_str().expect("Response had no URI").to_string();
139 let cid = body["cid"].as_str().expect("Response had no CID").to_string();
140 let rkey = uri.split('/').last().expect("URI was malformed").to_string();
141
142 (uri, cid, rkey)
143}
144
145pub async fn create_account_and_login(client: &Client) -> (String, String) {
146 let handle = format!("user_{}", uuid::Uuid::new_v4());
147 let payload = json!({
148 "handle": handle,
149 "email": format!("{}@example.com", handle),
150 "password": "password"
151 });
152
153 let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
154 .json(&payload)
155 .send()
156 .await
157 .expect("Failed to create account");
158
159 if res.status() != StatusCode::OK {
160 panic!("Failed to create account: {:?}", res.text().await);
161 }
162
163 let body: Value = res.json().await.expect("Invalid JSON");
164 let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
165 let did = body["did"].as_str().expect("No did").to_string();
166 (access_jwt, did)
167}