use std::borrow::Cow;
use std::collections::BTreeMap;
use testcontainers_modules::testcontainers::core::{
ContainerPort, ContainerState, ExecCommand, WaitFor,
};
use testcontainers_modules::testcontainers::{Image, TestcontainersError};
const NAME: &str = "supabase/edge-runtime";
const TAG: &str = "v1.67.4";
pub const FUNCTIONS_PORT: u16 = 9000;
const DEFAULT_MAIN_SERVICE_PATH: &str = "/home/deno/functions";
#[derive(Debug, Clone)]
pub struct Functions {
env_vars: BTreeMap<String, String>,
tag: String,
main_service_path: String,
}
impl Functions {
pub fn new() -> Self {
Self::default()
}
pub fn new_with_env(envs: BTreeMap<&str, &str>) -> Self {
let mut instance = Self::default();
for (key, val) in envs {
instance.env_vars.insert(key.to_string(), val.to_string());
}
instance
}
pub fn with_jwt_secret(mut self, secret: impl Into<String>) -> Self {
self.env_vars
.insert("JWT_SECRET".to_string(), secret.into());
self
}
pub fn with_supabase_url(mut self, url: impl Into<String>) -> Self {
self.env_vars.insert("SUPABASE_URL".to_string(), url.into());
self
}
pub fn with_anon_key(mut self, key: impl Into<String>) -> Self {
self.env_vars
.insert("SUPABASE_ANON_KEY".to_string(), key.into());
self
}
pub fn with_service_role_key(mut self, key: impl Into<String>) -> Self {
self.env_vars
.insert("SUPABASE_SERVICE_ROLE_KEY".to_string(), key.into());
self
}
pub fn with_db_url(mut self, url: impl Into<String>) -> Self {
self.env_vars
.insert("SUPABASE_DB_URL".to_string(), url.into());
self
}
pub fn with_verify_jwt(mut self, verify: bool) -> Self {
self.env_vars
.insert("VERIFY_JWT".to_string(), verify.to_string());
self
}
pub fn with_main_service_path(mut self, path: impl Into<String>) -> Self {
self.main_service_path = path.into();
self
}
pub fn with_port(mut self, port: u16) -> Self {
self.env_vars.insert("PORT".to_string(), port.to_string());
self
}
pub fn with_worker_timeout_ms(mut self, timeout: u64) -> Self {
self.env_vars
.insert("WORKER_TIMEOUT_MS".to_string(), timeout.to_string());
self
}
pub fn with_max_parallelism(mut self, max: u32) -> Self {
self.env_vars
.insert("MAX_PARALLELISM".to_string(), max.to_string());
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
}
}
impl Default for Functions {
fn default() -> Self {
let mut env_vars = BTreeMap::new();
env_vars.insert("PORT".to_string(), FUNCTIONS_PORT.to_string());
env_vars.insert("VERIFY_JWT".to_string(), "true".to_string());
Self {
env_vars,
tag: TAG.to_string(),
main_service_path: DEFAULT_MAIN_SERVICE_PATH.to_string(),
}
}
}
impl Image for Functions {
fn name(&self) -> &str {
NAME
}
fn tag(&self) -> &str {
&self.tag
}
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stdout("Listening on")]
}
fn expose_ports(&self) -> &[ContainerPort] {
&[ContainerPort::Tcp(FUNCTIONS_PORT)]
}
fn env_vars(
&self,
) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
&self.env_vars
}
fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
vec![
"start".to_string(),
"--main-service".to_string(),
self.main_service_path.clone(),
]
}
#[allow(unused_variables)]
fn exec_after_start(
&self,
cs: ContainerState,
) -> Result<Vec<ExecCommand>, TestcontainersError> {
Ok(vec![])
}
}
#[cfg(test)]
#[cfg(feature = "functions")]
mod tests {
use super::*;
use std::borrow::Cow;
use testcontainers_modules::testcontainers::Image;
#[test]
fn test_default_configuration() {
let functions = Functions::default();
assert_eq!(functions.env_vars.get("PORT"), Some(&"9000".to_string()));
assert_eq!(
functions.env_vars.get("VERIFY_JWT"),
Some(&"true".to_string())
);
assert_eq!(
functions.main_service_path,
"/home/deno/functions".to_string()
);
}
#[test]
fn test_name_returns_correct_image() {
let functions = Functions::default();
assert_eq!(functions.name(), "supabase/edge-runtime");
}
#[test]
fn test_tag_returns_correct_version() {
let functions = Functions::default();
assert_eq!(functions.tag(), TAG);
}
#[test]
fn test_functions_port_constant() {
assert_eq!(FUNCTIONS_PORT, 9000);
}
#[test]
fn test_with_jwt_secret() {
let functions =
Functions::default().with_jwt_secret("super-secret-jwt-token-with-at-least-32-chars");
assert_eq!(
functions.env_vars.get("JWT_SECRET"),
Some(&"super-secret-jwt-token-with-at-least-32-chars".to_string())
);
}
#[test]
fn test_with_supabase_url() {
let functions = Functions::default().with_supabase_url("http://kong:8000");
assert_eq!(
functions.env_vars.get("SUPABASE_URL"),
Some(&"http://kong:8000".to_string())
);
}
#[test]
fn test_with_anon_key() {
let functions = Functions::default().with_anon_key("anon-jwt-token");
assert_eq!(
functions.env_vars.get("SUPABASE_ANON_KEY"),
Some(&"anon-jwt-token".to_string())
);
}
#[test]
fn test_with_service_role_key() {
let functions = Functions::default().with_service_role_key("service-role-jwt-token");
assert_eq!(
functions.env_vars.get("SUPABASE_SERVICE_ROLE_KEY"),
Some(&"service-role-jwt-token".to_string())
);
}
#[test]
fn test_with_db_url() {
let functions = Functions::default().with_db_url("postgres://user:pass@localhost:5432/db");
assert_eq!(
functions.env_vars.get("SUPABASE_DB_URL"),
Some(&"postgres://user:pass@localhost:5432/db".to_string())
);
}
#[test]
fn test_with_verify_jwt() {
let functions = Functions::default().with_verify_jwt(false);
assert_eq!(
functions.env_vars.get("VERIFY_JWT"),
Some(&"false".to_string())
);
}
#[test]
fn test_with_main_service_path() {
let functions = Functions::default().with_main_service_path("/custom/functions/path");
assert_eq!(
functions.main_service_path,
"/custom/functions/path".to_string()
);
}
#[test]
fn test_with_port() {
let functions = Functions::default().with_port(8080);
assert_eq!(functions.env_vars.get("PORT"), Some(&"8080".to_string()));
}
#[test]
fn test_with_worker_timeout_ms() {
let functions = Functions::default().with_worker_timeout_ms(30000);
assert_eq!(
functions.env_vars.get("WORKER_TIMEOUT_MS"),
Some(&"30000".to_string())
);
}
#[test]
fn test_with_max_parallelism() {
let functions = Functions::default().with_max_parallelism(4);
assert_eq!(
functions.env_vars.get("MAX_PARALLELISM"),
Some(&"4".to_string())
);
}
#[test]
fn test_with_tag_overrides_default() {
let functions = Functions::default().with_tag("v1.0.0");
assert_eq!(functions.tag(), "v1.0.0");
}
#[test]
fn test_with_env_adds_custom_variable() {
let functions = Functions::default()
.with_env("CUSTOM_VAR", "custom_value")
.with_env("ANOTHER_VAR", "another_value");
assert_eq!(
functions.env_vars.get("CUSTOM_VAR"),
Some(&"custom_value".to_string())
);
assert_eq!(
functions.env_vars.get("ANOTHER_VAR"),
Some(&"another_value".to_string())
);
}
#[test]
fn test_builder_method_chaining() {
let functions = Functions::default()
.with_jwt_secret("my-jwt-secret")
.with_supabase_url("http://kong:8000")
.with_anon_key("anon-key")
.with_service_role_key("service-key")
.with_db_url("postgres://user:pass@localhost:5432/db")
.with_verify_jwt(false)
.with_main_service_path("/custom/path")
.with_tag("v2.0.0");
assert_eq!(
functions.env_vars.get("JWT_SECRET"),
Some(&"my-jwt-secret".to_string())
);
assert_eq!(
functions.env_vars.get("SUPABASE_URL"),
Some(&"http://kong:8000".to_string())
);
assert_eq!(
functions.env_vars.get("SUPABASE_ANON_KEY"),
Some(&"anon-key".to_string())
);
assert_eq!(
functions.env_vars.get("SUPABASE_SERVICE_ROLE_KEY"),
Some(&"service-key".to_string())
);
assert_eq!(
functions.env_vars.get("SUPABASE_DB_URL"),
Some(&"postgres://user:pass@localhost:5432/db".to_string())
);
assert_eq!(
functions.env_vars.get("VERIFY_JWT"),
Some(&"false".to_string())
);
assert_eq!(functions.main_service_path, "/custom/path".to_string());
assert_eq!(functions.tag(), "v2.0.0");
}
#[test]
fn test_new_creates_default_instance() {
let functions = Functions::new();
assert_eq!(functions.name(), NAME);
assert_eq!(functions.tag(), TAG);
}
#[test]
fn test_new_with_env() {
let mut envs = BTreeMap::new();
envs.insert("CUSTOM_KEY", "custom_value");
envs.insert("PORT", "8080");
let functions = Functions::new_with_env(envs);
assert_eq!(
functions.env_vars.get("CUSTOM_KEY"),
Some(&"custom_value".to_string())
);
assert_eq!(functions.env_vars.get("PORT"), Some(&"8080".to_string()));
}
#[test]
fn test_expose_ports() {
let functions = Functions::default();
let ports = functions.expose_ports();
assert_eq!(ports.len(), 1);
assert_eq!(ports[0], ContainerPort::Tcp(9000));
}
#[test]
fn test_ready_conditions() {
let functions = Functions::default();
let conditions = functions.ready_conditions();
assert_eq!(conditions.len(), 1);
}
#[test]
fn test_cmd_returns_correct_startup_command() {
let functions = Functions::default();
let cmd: Vec<Cow<'_, str>> = functions.cmd().into_iter().map(|s| s.into()).collect();
assert_eq!(cmd.len(), 3);
assert_eq!(cmd[0], "start");
assert_eq!(cmd[1], "--main-service");
assert_eq!(cmd[2], "/home/deno/functions");
}
#[test]
fn test_cmd_with_custom_path() {
let functions = Functions::default().with_main_service_path("/custom/functions");
let cmd: Vec<Cow<'_, str>> = functions.cmd().into_iter().map(|s| s.into()).collect();
assert_eq!(cmd[2], "/custom/functions");
}
}