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