use anyhow::Result;
use std::sync::atomic::{AtomicU64, Ordering};
use supabase_testcontainers_modules::{Auth, AUTH_PORT, LOCAL_HOST};
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, ImageExt};
use testcontainers_modules::postgres::Postgres;
use tokio_postgres::NoTls;
const POSTGRES_PORT: u16 = 5432;
const TEST_NETWORK: &str = "supabase-test-network";
const POSTGRES_ALIAS: &str = "postgres";
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)
}
pub struct AuthTestContext {
pub postgres: ContainerAsync<Postgres>,
pub auth: ContainerAsync<Auth>,
pub auth_port: u16,
pub postgres_port: u16,
}
pub async fn setup_auth_with_postgres() -> Result<AuthTestContext> {
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 auth_db_url = format!(
"postgres://supabase_auth_admin:testpassword@{}:{}/postgres",
postgres_name, POSTGRES_PORT
);
let local_db_url = format!(
"postgres://postgres:postgres@{}:{}/postgres",
LOCAL_HOST, postgres_port
);
let auth = Auth::default()
.with_db_url(&auth_db_url)
.with_mailer_autoconfirm(true)
.with_sms_autoconfirm(true)
.with_anonymous_users(true)
.init_db_schema(&local_db_url, "testpassword")
.await?
.with_network(&network_name)
.start()
.await?;
let auth_port = auth.get_host_port_ipv4(AUTH_PORT).await?;
Ok(AuthTestContext {
postgres,
auth,
auth_port,
postgres_port,
})
}
pub fn auth_url(port: u16) -> String {
format!("http://{}:{}", LOCAL_HOST, port)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_containers_start() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
assert!(ctx.auth_port > 0);
assert!(ctx.postgres_port > 0);
Ok(())
}
#[tokio::test]
async fn test_health_endpoint_returns_200() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let response = reqwest::get(format!("{}/health", auth_url(ctx.auth_port))).await?;
assert_eq!(response.status(), 200);
Ok(())
}
#[tokio::test]
async fn test_settings_endpoint_returns_config() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let response = reqwest::get(format!("{}/settings", auth_url(ctx.auth_port))).await?;
assert_eq!(response.status(), 200);
let settings: serde_json::Value = response.json().await?;
assert!(settings.get("external").is_some());
assert!(settings.get("disable_signup").is_some());
Ok(())
}
#[tokio::test]
async fn test_anonymous_signup() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let client = reqwest::Client::new();
let response = client
.post(format!("{}/signup", auth_url(ctx.auth_port)))
.header("Content-Type", "application/json")
.json(&serde_json::json!({}))
.send()
.await?;
assert!(response.status().is_success());
let body: serde_json::Value = response.json().await?;
assert!(body.get("access_token").is_some());
Ok(())
}
#[tokio::test]
async fn test_email_signup_with_autoconfirm() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let client = reqwest::Client::new();
let response = client
.post(format!("{}/signup", auth_url(ctx.auth_port)))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": "test@example.com",
"password": "testpassword123"
}))
.send()
.await?;
assert!(response.status().is_success());
let body: serde_json::Value = response.json().await?;
assert!(body.get("access_token").is_some());
assert!(body.get("refresh_token").is_some());
Ok(())
}
#[tokio::test]
async fn test_signup_rejected_when_disabled() -> Result<()> {
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 auth_db_url = format!(
"postgres://supabase_auth_admin:testpassword@{}:{}/postgres",
postgres_name, POSTGRES_PORT
);
let local_db_url = format!(
"postgres://postgres:postgres@{}:{}/postgres",
LOCAL_HOST, postgres_port
);
let auth = Auth::default()
.with_db_url(&auth_db_url)
.with_signup_disabled(true)
.init_db_schema(&local_db_url, "testpassword")
.await?
.with_network(&network_name)
.start()
.await?;
let auth_port = auth.get_host_port_ipv4(AUTH_PORT).await?;
let client = reqwest::Client::new();
let response = client
.post(format!("{}/signup", auth_url(auth_port)))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": "test@example.com",
"password": "testpassword123"
}))
.send()
.await?;
assert!(!response.status().is_success());
Ok(())
}
#[tokio::test]
async fn test_token_refresh() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let client = reqwest::Client::new();
let signup_response = client
.post(format!("{}/signup", auth_url(ctx.auth_port)))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": "refresh@example.com",
"password": "testpassword123"
}))
.send()
.await?;
assert!(signup_response.status().is_success());
let signup_body: serde_json::Value = signup_response.json().await?;
let refresh_token = signup_body["refresh_token"]
.as_str()
.expect("refresh_token should be present");
let refresh_response = client
.post(format!(
"{}/token?grant_type=refresh_token",
auth_url(ctx.auth_port)
))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"refresh_token": refresh_token
}))
.send()
.await?;
assert!(refresh_response.status().is_success());
let refresh_body: serde_json::Value = refresh_response.json().await?;
assert!(refresh_body.get("access_token").is_some());
Ok(())
}
#[tokio::test]
async fn test_user_retrieval_with_token() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let client = reqwest::Client::new();
let signup_response = client
.post(format!("{}/signup", auth_url(ctx.auth_port)))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": "user@example.com",
"password": "testpassword123"
}))
.send()
.await?;
assert!(signup_response.status().is_success());
let signup_body: serde_json::Value = signup_response.json().await?;
let access_token = signup_body["access_token"]
.as_str()
.expect("access_token should be present");
let user_response = client
.get(format!("{}/user", auth_url(ctx.auth_port)))
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
assert!(user_response.status().is_success());
let user_body: serde_json::Value = user_response.json().await?;
assert_eq!(user_body["email"].as_str(), Some("user@example.com"));
Ok(())
}
#[tokio::test]
async fn test_auth_schema_created() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let db_url = format!(
"postgres://postgres:postgres@{}:{}/postgres",
LOCAL_HOST, 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 = 'auth'",
&[],
)
.await?;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get::<_, String>(0), "auth");
Ok(())
}
#[tokio::test]
async fn test_auth_admin_user_created() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let db_url = format!(
"postgres://postgres:postgres@{}:{}/postgres",
LOCAL_HOST, 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 usename FROM pg_user WHERE usename = 'supabase_auth_admin'",
&[],
)
.await?;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get::<_, String>(0), "supabase_auth_admin");
Ok(())
}
#[tokio::test]
async fn test_migrations_run_successfully() -> Result<()> {
let ctx = setup_auth_with_postgres().await?;
let response = reqwest::get(format!("{}/health", auth_url(ctx.auth_port))).await?;
assert_eq!(response.status(), 200);
let db_url = format!(
"postgres://postgres:postgres@{}:{}/postgres",
LOCAL_HOST, 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 table_name FROM information_schema.tables
WHERE table_schema = 'auth' AND table_name = 'users'",
&[],
)
.await?;
assert_eq!(rows.len(), 1);
Ok(())
}
}