relay filter/appview bootstrap
1use prism::{AppState, db::Database};
2use reqwest::Client;
3use sqlx::postgres::PgPoolOptions;
4use std::sync::{Arc, OnceLock};
5use tokio::net::TcpListener;
6use tokio::sync::broadcast;
7
8static SERVER_URL: OnceLock<String> = OnceLock::new();
9static APP_PORT: OnceLock<u16> = OnceLock::new();
10
11use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
12use testcontainers_modules::postgres::Postgres;
13
14static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
15
16fn has_external_infra() -> bool {
17 std::env::var("PRISM_TEST_INFRA_READY").is_ok()
18}
19
20#[cfg(test)]
21#[ctor::dtor]
22fn cleanup() {
23 if has_external_infra() {
24 return;
25 }
26
27 if std::env::var("XDG_RUNTIME_DIR").is_ok() {
28 let _ = std::process::Command::new("podman")
29 .args(["rm", "-f", "--filter", "label=prism_test=true"])
30 .output();
31 }
32
33 let _ = std::process::Command::new("docker")
34 .args(["container", "prune", "-f", "--filter", "label=prism_test=true"])
35 .output();
36}
37
38#[allow(dead_code)]
39pub fn client() -> Client {
40 Client::new()
41}
42
43#[allow(dead_code)]
44pub fn app_port() -> u16 {
45 *APP_PORT.get().expect("APP_PORT not initialized")
46}
47
48pub async fn base_url() -> &'static str {
49 SERVER_URL.get_or_init(|| {
50 let (tx, rx) = std::sync::mpsc::channel();
51
52 std::thread::spawn(move || {
53 if std::env::var("DOCKER_HOST").is_err() {
54 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
55 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
56 if podman_sock.exists() {
57 unsafe {
58 std::env::set_var(
59 "DOCKER_HOST",
60 format!("unix://{}", podman_sock.display()),
61 );
62 }
63 }
64 }
65 }
66
67 let rt = tokio::runtime::Runtime::new().unwrap();
68 rt.block_on(async move {
69 if has_external_infra() {
70 let url = setup_with_external_infra().await;
71 tx.send(url).unwrap();
72 } else {
73 let url = setup_with_testcontainers().await;
74 tx.send(url).unwrap();
75 }
76 std::future::pending::<()>().await;
77 });
78 });
79
80 rx.recv().expect("Failed to start test server")
81 })
82}
83
84async fn setup_with_external_infra() -> String {
85 let database_url =
86 std::env::var("DATABASE_URL").expect("DATABASE_URL must be set when using external infra");
87
88 spawn_app(database_url).await
89}
90
91async fn setup_with_testcontainers() -> String {
92 let container = Postgres::default()
93 .with_tag("18-alpine")
94 .with_label("prism_test", "true")
95 .start()
96 .await
97 .expect("Failed to start Postgres");
98
99 let connection_string = format!(
100 "postgres://postgres:postgres@127.0.0.1:{}/postgres",
101 container
102 .get_host_port_ipv4(5432)
103 .await
104 .expect("Failed to get port")
105 );
106
107 DB_CONTAINER.set(container).ok();
108
109 spawn_app(connection_string).await
110}
111
112async fn spawn_app(database_url: String) -> String {
113 let pool = PgPoolOptions::new()
114 .max_connections(10)
115 .connect(&database_url)
116 .await
117 .expect("Failed to connect to Postgres");
118
119 sqlx::migrate!("./migrations")
120 .run(&pool)
121 .await
122 .expect("Failed to run migrations");
123
124 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
125 let addr = listener.local_addr().unwrap();
126 APP_PORT.set(addr.port()).ok();
127
128 let (tx, _rx) = broadcast::channel::<String>(1024);
129 let broadcaster = prism::ws::Broadcaster::new(tx);
130
131 let state = Arc::new(AppState {
132 db: Database::new(pool),
133 broadcaster,
134 });
135
136 let app = prism::api::router()
137 .merge(prism::ws::router())
138 .with_state(state);
139
140 tokio::spawn(async move {
141 axum::serve(listener, app).await.unwrap();
142 });
143
144 format!("http://{}", addr)
145}
146
147#[allow(dead_code)]
148pub async fn get_db_pool() -> sqlx::PgPool {
149 base_url().await;
150
151 let connection_string = if has_external_infra() {
152 std::env::var("DATABASE_URL").expect("DATABASE_URL not set")
153 } else {
154 let container = DB_CONTAINER.get().expect("DB container not initialized");
155 let port = container
156 .get_host_port_ipv4(5432)
157 .await
158 .expect("Failed to get port");
159 format!("postgres://postgres:postgres@127.0.0.1:{}/postgres", port)
160 };
161
162 PgPoolOptions::new()
163 .max_connections(5)
164 .connect(&connection_string)
165 .await
166 .expect("Failed to connect to test database")
167}