use anyhow::Result;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use supabase_testcontainers_modules::{Analytics, ANALYTICS_PORT, LOCAL_HOST};
use testcontainers::runners::AsyncRunner;
use testcontainers::ImageExt;
use testcontainers_modules::postgres::Postgres;
use tokio_postgres::NoTls;
const POSTGRES_PORT: u16 = 5432;
const TEST_NETWORK: &str = "analytics-test-network";
const POSTGRES_ALIAS: &str = "analytics-db";
const PUBLIC_ACCESS_TOKEN: &str = "test-public-access-token";
const PRIVATE_ACCESS_TOKEN: &str = "test-private-access-token";
const ENCRYPTION_KEY: &str = "dGhpcy1pcy1hLTMyLWJ5dGUtZW5jcnlwdGlvbi1rZXk=";
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn unique_test_id() -> String {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
let counter = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
format!("{}-{}", timestamp, counter)
}
fn postgres_url(port: u16) -> String {
format!(
"postgres://postgres:postgres@{}:{}/postgres",
LOCAL_HOST, port
)
}
#[cfg(test)]
mod tests {
use super::*;
use testcontainers::ContainerAsync;
struct AnalyticsContext {
#[allow(dead_code)]
postgres: ContainerAsync<Postgres>,
#[allow(dead_code)]
analytics: ContainerAsync<Analytics>,
postgres_port: u16,
#[allow(dead_code)]
analytics_port: u16,
}
async fn setup_analytics() -> Result<AnalyticsContext> {
let test_id = unique_test_id();
let network_name = format!("{}-{}", TEST_NETWORK, test_id);
let postgres_name = format!("{}-{}", POSTGRES_ALIAS, test_id);
let postgres = Postgres::default()
.with_tag("15-alpine")
.with_network(&network_name)
.with_container_name(&postgres_name)
.start()
.await?;
let postgres_port = postgres.get_host_port_ipv4(POSTGRES_PORT).await?;
let db_url = postgres_url(postgres_port);
setup_analytics_schema(&db_url).await?;
let analytics_db_url = format!(
"postgresql://postgres:postgres@{}:{}/postgres",
postgres_name, POSTGRES_PORT
);
let analytics = Analytics::default()
.with_postgres_backend_url(&analytics_db_url)
.with_db_hostname(&postgres_name)
.with_db_port(POSTGRES_PORT)
.with_db_username("postgres")
.with_db_password("postgres")
.with_db_database("postgres")
.with_public_access_token(PUBLIC_ACCESS_TOKEN)
.with_private_access_token(PRIVATE_ACCESS_TOKEN)
.with_encryption_key(ENCRYPTION_KEY)
.with_startup_timeout(Duration::from_secs(120))
.with_network(&network_name)
.start()
.await?;
let analytics_port = analytics.get_host_port_ipv4(ANALYTICS_PORT).await?;
tokio::time::sleep(Duration::from_secs(5)).await;
Ok(AnalyticsContext {
postgres,
analytics,
postgres_port,
analytics_port,
})
}
async fn setup_analytics_schema(db_url: &str) -> Result<()> {
let (client, connection) = tokio_postgres::connect(db_url, NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
client
.batch_execute(
r#"
-- Create roles that analytics might expect
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon') THEN
CREATE ROLE anon NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN
CREATE ROLE authenticated NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'service_role') THEN
CREATE ROLE service_role NOLOGIN;
END IF;
END
$$;
-- Create extensions that Logflare needs
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
"#,
)
.await?;
Ok(())
}
#[tokio::test]
async fn test_containers_start() -> Result<()> {
let ctx = setup_analytics().await?;
assert!(ctx.postgres_port > 0, "PostgreSQL port should be assigned");
assert!(ctx.analytics_port > 0, "Analytics port should be assigned");
Ok(())
}
#[tokio::test]
async fn test_postgres_connection_works() -> Result<()> {
let ctx = setup_analytics().await?;
let db_url = postgres_url(ctx.postgres_port);
let (client, connection) = tokio_postgres::connect(&db_url, NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
let rows = client.query("SELECT 1 as value", &[]).await?;
assert_eq!(rows.len(), 1);
Ok(())
}
#[tokio::test]
async fn test_analytics_schema_check() -> Result<()> {
let ctx = setup_analytics().await?;
let db_url = postgres_url(ctx.postgres_port);
let (client, connection) = tokio_postgres::connect(&db_url, NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
let rows = client
.query(
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = '_analytics'",
&[],
)
.await?;
if rows.is_empty() {
println!("Note: _analytics schema not found - this is expected if migrations are still running");
} else {
println!("_analytics schema found - Logflare migrations completed");
}
Ok(())
}
}