use std::borrow::Cow;
use std::collections::BTreeMap;
use anyhow::{bail, Context};
use testcontainers_modules::testcontainers::core::{
ContainerPort, ContainerState, ExecCommand, WaitFor,
};
use testcontainers_modules::testcontainers::{Image, TestcontainersError};
use tokio_postgres::NoTls;
const NAME: &str = "supabase/gotrue";
const TAG: &str = "v2.183.0";
pub const AUTH_PORT: u16 = 9999;
#[cfg(feature = "auth")]
#[derive(Debug, Clone)]
pub struct Auth {
env_vars: BTreeMap<String, String>,
tag: String,
}
impl Auth {
pub fn new(postgres_connection_string: impl Into<String>) -> Self {
Self::default().with_db_url(postgres_connection_string)
}
pub fn with_db_url(mut self, url: impl Into<String>) -> Self {
self.env_vars.insert("DATABASE_URL".to_string(), url.into());
self
}
pub fn with_jwt_secret(mut self, secret: impl Into<String>) -> Self {
self.env_vars
.insert("GOTRUE_JWT_SECRET".to_string(), secret.into());
self
}
pub fn with_jwt_expiry(mut self, seconds: u32) -> Self {
self.env_vars
.insert("GOTRUE_JWT_EXP".to_string(), seconds.to_string());
self
}
pub fn with_api_external_url(mut self, url: impl Into<String>) -> Self {
self.env_vars
.insert("API_EXTERNAL_URL".to_string(), url.into());
self
}
pub fn with_site_url(mut self, url: impl Into<String>) -> Self {
self.env_vars
.insert("GOTRUE_SITE_URL".to_string(), url.into());
self
}
pub fn with_signup_disabled(mut self, disabled: bool) -> Self {
self.env_vars
.insert("GOTRUE_DISABLE_SIGNUP".to_string(), disabled.to_string());
self
}
pub fn with_anonymous_users(mut self, enabled: bool) -> Self {
self.env_vars.insert(
"GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED".to_string(),
enabled.to_string(),
);
self
}
pub fn with_mailer_autoconfirm(mut self, enabled: bool) -> Self {
self.env_vars
.insert("GOTRUE_MAILER_AUTOCONFIRM".to_string(), enabled.to_string());
self
}
pub fn with_sms_autoconfirm(mut self, enabled: bool) -> Self {
self.env_vars
.insert("GOTRUE_SMS_AUTOCONFIRM".to_string(), enabled.to_string());
self
}
pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
self.env_vars
.insert("GOTRUE_LOG_LEVEL".to_string(), level.into());
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = tag.into();
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}
pub fn git_release_version(&self) -> String {
let version = self.tag[1..].to_string();
format!("release/{}", version)
}
pub async fn init_db_schema(
self,
db_url: &str,
auth_admin_password: &str,
) -> anyhow::Result<Self> {
if db_url.is_empty() {
bail!("database URL cannot be empty");
}
let db_schema = self
.env_vars
.get("DB_NAMESPACE")
.map(|s| s.as_str())
.unwrap_or("auth");
let (client, connection) = tokio_postgres::connect(db_url, NoTls)
.await
.with_context(|| format!("failed to connect to PostgreSQL at {}", db_url))?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("PostgreSQL connection error: {}", e);
}
});
let query = format!(
"CREATE USER supabase_admin LOGIN CREATEROLE CREATEDB REPLICATION BYPASSRLS;
CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD '{auth_admin_password}';
CREATE SCHEMA IF NOT EXISTS {db_schema} AUTHORIZATION supabase_auth_admin;
GRANT CREATE ON DATABASE postgres TO supabase_auth_admin;
ALTER USER supabase_auth_admin SET search_path = '{db_schema}';"
);
client
.batch_execute(&query)
.await
.context("failed to initialize auth database schema")?;
Ok(self)
}
}
impl Default for Auth {
fn default() -> Self {
let mut env_vars = BTreeMap::new();
env_vars.insert("GOTRUE_DB_DRIVER".to_string(), "postgres".to_string());
env_vars.insert("DB_NAMESPACE".to_string(), "auth".to_string());
env_vars.insert(
"GOTRUE_JWT_SECRET".to_string(),
"super-secret-jwt-token-for-testing-at-least-32-chars".to_string(),
);
env_vars.insert("GOTRUE_JWT_EXP".to_string(), "3600".to_string());
env_vars.insert("GOTRUE_API_HOST".to_string(), "0.0.0.0".to_string());
env_vars.insert("PORT".to_string(), "9999".to_string());
env_vars.insert(
"API_EXTERNAL_URL".to_string(),
"http://localhost:9999".to_string(),
);
env_vars.insert(
"GOTRUE_SITE_URL".to_string(),
"http://localhost:3000".to_string(),
);
env_vars.insert("GOTRUE_DISABLE_SIGNUP".to_string(), "false".to_string());
env_vars.insert(
"GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED".to_string(),
"true".to_string(),
);
env_vars.insert("GOTRUE_MAILER_AUTOCONFIRM".to_string(), "true".to_string());
env_vars.insert("GOTRUE_SMS_AUTOCONFIRM".to_string(), "true".to_string());
env_vars.insert("GOTRUE_LOG_LEVEL".to_string(), "debug".to_string());
Self {
env_vars,
tag: TAG.to_string(),
}
}
}
impl Image for Auth {
fn name(&self) -> &str {
NAME
}
fn tag(&self) -> &str {
&self.tag
}
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stderr("API started")]
}
fn expose_ports(&self) -> &[ContainerPort] {
&[ContainerPort::Tcp(AUTH_PORT)]
}
fn env_vars(
&self,
) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
&self.env_vars
}
#[allow(unused_variables)]
fn exec_after_start(
&self,
cs: ContainerState,
) -> Result<Vec<ExecCommand>, TestcontainersError> {
Ok(vec![])
}
}
#[cfg(test)]
mod tests {
use super::*;
use testcontainers_modules::testcontainers::Image;
#[test]
fn test_default_configuration() {
let auth = Auth::default();
assert_eq!(
auth.env_vars.get("GOTRUE_DB_DRIVER"),
Some(&"postgres".to_string())
);
assert_eq!(auth.env_vars.get("DB_NAMESPACE"), Some(&"auth".to_string()));
assert_eq!(auth.env_vars.get("PORT"), Some(&"9999".to_string()));
assert_eq!(
auth.env_vars.get("GOTRUE_API_HOST"),
Some(&"0.0.0.0".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_MAILER_AUTOCONFIRM"),
Some(&"true".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_SMS_AUTOCONFIRM"),
Some(&"true".to_string())
);
}
#[test]
fn test_name_returns_correct_image() {
let auth = Auth::default();
assert_eq!(auth.name(), "supabase/gotrue");
}
#[test]
fn test_tag_returns_correct_version() {
let auth = Auth::default();
assert_eq!(auth.tag(), "v2.183.0");
}
#[test]
fn test_builder_method_chaining() {
let auth = Auth::default()
.with_db_url("postgres://user:pass@localhost:5432/db")
.with_jwt_secret("my-secret-key")
.with_jwt_expiry(7200)
.with_site_url("http://example.com")
.with_signup_disabled(true)
.with_anonymous_users(false)
.with_log_level("info");
assert_eq!(
auth.env_vars.get("DATABASE_URL"),
Some(&"postgres://user:pass@localhost:5432/db".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_JWT_SECRET"),
Some(&"my-secret-key".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_JWT_EXP"),
Some(&"7200".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_SITE_URL"),
Some(&"http://example.com".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_DISABLE_SIGNUP"),
Some(&"true".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED"),
Some(&"false".to_string())
);
assert_eq!(
auth.env_vars.get("GOTRUE_LOG_LEVEL"),
Some(&"info".to_string())
);
}
#[test]
fn test_with_tag_overrides_default() {
let auth = Auth::default().with_tag("v2.100.0");
assert_eq!(auth.tag(), "v2.100.0");
}
#[test]
fn test_with_env_adds_custom_variable() {
let auth = Auth::default()
.with_env("CUSTOM_VAR", "custom_value")
.with_env("ANOTHER_VAR", "another_value");
assert_eq!(
auth.env_vars.get("CUSTOM_VAR"),
Some(&"custom_value".to_string())
);
assert_eq!(
auth.env_vars.get("ANOTHER_VAR"),
Some(&"another_value".to_string())
);
}
#[test]
fn test_new_sets_database_url() {
let auth = Auth::new("postgres://test:test@localhost:5432/testdb");
assert_eq!(
auth.env_vars.get("DATABASE_URL"),
Some(&"postgres://test:test@localhost:5432/testdb".to_string())
);
}
#[test]
fn test_auth_port_constant() {
assert_eq!(AUTH_PORT, 9999);
}
}