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/storage-api";
const TAG: &str = "v1.11.1";
pub const STORAGE_PORT: u16 = 5000;
#[derive(Debug, Clone)]
pub struct Storage {
env_vars: BTreeMap<String, String>,
tag: String,
}
impl Storage {
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_database_url(mut self, url: impl Into<String>) -> Self {
self.env_vars.insert("DATABASE_URL".to_string(), url.into());
self
}
pub fn with_storage_backend(mut self, backend: impl Into<String>) -> Self {
self.env_vars
.insert("STORAGE_BACKEND".to_string(), backend.into());
self
}
pub fn with_anon_key(mut self, key: impl Into<String>) -> Self {
self.env_vars.insert("ANON_KEY".to_string(), key.into());
self
}
pub fn with_service_key(mut self, key: impl Into<String>) -> Self {
self.env_vars.insert("SERVICE_KEY".to_string(), key.into());
self
}
pub fn with_jwt_secret(mut self, secret: impl Into<String>) -> Self {
self.env_vars
.insert("PGRST_JWT_SECRET".to_string(), secret.into());
self
}
pub fn with_postgrest_url(mut self, url: impl Into<String>) -> Self {
self.env_vars
.insert("POSTGREST_URL".to_string(), url.into());
self
}
pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
self.env_vars
.insert("TENANT_ID".to_string(), tenant_id.into());
self
}
pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.env_vars.insert("REGION".to_string(), region.into());
self
}
pub fn with_global_s3_bucket(mut self, bucket: impl Into<String>) -> Self {
self.env_vars
.insert("GLOBAL_S3_BUCKET".to_string(), bucket.into());
self
}
pub fn with_file_size_limit(mut self, limit: u64) -> Self {
self.env_vars
.insert("FILE_SIZE_LIMIT".to_string(), limit.to_string());
self
}
pub fn with_file_storage_path(mut self, path: impl Into<String>) -> Self {
self.env_vars
.insert("FILE_STORAGE_BACKEND_PATH".to_string(), path.into());
self
}
pub fn with_upload_signed_url_expiration(mut self, seconds: u32) -> Self {
self.env_vars.insert(
"UPLOAD_SIGNED_URL_EXPIRATION_TIME".to_string(),
seconds.to_string(),
);
self
}
pub fn with_multitenant(mut self, enabled: bool) -> Self {
self.env_vars
.insert("IS_MULTITENANT".to_string(), enabled.to_string());
self
}
pub fn with_tus_url_path(mut self, path: impl Into<String>) -> Self {
self.env_vars
.insert("TUS_URL_PATH".to_string(), path.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
}
}
impl Default for Storage {
fn default() -> Self {
let mut env_vars = BTreeMap::new();
env_vars.insert("PORT".to_string(), STORAGE_PORT.to_string());
env_vars.insert("REGION".to_string(), "local".to_string());
env_vars.insert("STORAGE_BACKEND".to_string(), "file".to_string());
env_vars.insert(
"FILE_STORAGE_BACKEND_PATH".to_string(),
"/var/lib/storage".to_string(),
);
env_vars.insert("FILE_SIZE_LIMIT".to_string(), "52428800".to_string()); env_vars.insert("GLOBAL_S3_BUCKET".to_string(), "storage".to_string());
env_vars.insert("TENANT_ID".to_string(), "default".to_string());
env_vars.insert("IS_MULTITENANT".to_string(), "false".to_string());
Self {
env_vars,
tag: TAG.to_string(),
}
}
}
impl Image for Storage {
fn name(&self) -> &str {
NAME
}
fn tag(&self) -> &str {
&self.tag
}
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stdout("[Server] Started Successfully")]
}
fn expose_ports(&self) -> &[ContainerPort] {
&[ContainerPort::Tcp(STORAGE_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)]
#[cfg(feature = "storage")]
mod tests {
use super::*;
use testcontainers_modules::testcontainers::Image;
#[test]
fn test_default_configuration() {
let storage = Storage::default();
assert_eq!(
storage.env_vars.get("STORAGE_BACKEND"),
Some(&"file".to_string())
);
assert_eq!(storage.env_vars.get("REGION"), Some(&"local".to_string()));
assert_eq!(storage.env_vars.get("PORT"), Some(&"5000".to_string()));
assert_eq!(
storage.env_vars.get("FILE_STORAGE_BACKEND_PATH"),
Some(&"/var/lib/storage".to_string())
);
assert_eq!(
storage.env_vars.get("FILE_SIZE_LIMIT"),
Some(&"52428800".to_string())
);
assert_eq!(
storage.env_vars.get("TENANT_ID"),
Some(&"default".to_string())
);
assert_eq!(
storage.env_vars.get("IS_MULTITENANT"),
Some(&"false".to_string())
);
}
#[test]
fn test_name_returns_correct_image() {
let storage = Storage::default();
assert_eq!(storage.name(), "supabase/storage-api");
}
#[test]
fn test_tag_returns_correct_version() {
let storage = Storage::default();
assert_eq!(storage.tag(), "v1.11.1");
}
#[test]
fn test_storage_port_constant() {
assert_eq!(STORAGE_PORT, 5000);
}
#[test]
fn test_with_database_url() {
let storage =
Storage::default().with_database_url("postgres://user:pass@localhost:5432/db");
assert_eq!(
storage.env_vars.get("DATABASE_URL"),
Some(&"postgres://user:pass@localhost:5432/db".to_string())
);
}
#[test]
fn test_with_storage_backend() {
let storage = Storage::default().with_storage_backend("s3");
assert_eq!(
storage.env_vars.get("STORAGE_BACKEND"),
Some(&"s3".to_string())
);
}
#[test]
fn test_with_anon_key() {
let storage = Storage::default().with_anon_key("anon-jwt-token");
assert_eq!(
storage.env_vars.get("ANON_KEY"),
Some(&"anon-jwt-token".to_string())
);
}
#[test]
fn test_with_service_key() {
let storage = Storage::default().with_service_key("service-role-jwt-token");
assert_eq!(
storage.env_vars.get("SERVICE_KEY"),
Some(&"service-role-jwt-token".to_string())
);
}
#[test]
fn test_with_jwt_secret() {
let storage = Storage::default().with_jwt_secret("my-jwt-secret");
assert_eq!(
storage.env_vars.get("PGRST_JWT_SECRET"),
Some(&"my-jwt-secret".to_string())
);
}
#[test]
fn test_with_postgrest_url() {
let storage = Storage::default().with_postgrest_url("http://postgrest:3000");
assert_eq!(
storage.env_vars.get("POSTGREST_URL"),
Some(&"http://postgrest:3000".to_string())
);
}
#[test]
fn test_with_tenant_id() {
let storage = Storage::default().with_tenant_id("my-tenant");
assert_eq!(
storage.env_vars.get("TENANT_ID"),
Some(&"my-tenant".to_string())
);
}
#[test]
fn test_with_region() {
let storage = Storage::default().with_region("us-east-1");
assert_eq!(
storage.env_vars.get("REGION"),
Some(&"us-east-1".to_string())
);
}
#[test]
fn test_with_global_s3_bucket() {
let storage = Storage::default().with_global_s3_bucket("my-bucket");
assert_eq!(
storage.env_vars.get("GLOBAL_S3_BUCKET"),
Some(&"my-bucket".to_string())
);
}
#[test]
fn test_with_file_size_limit() {
let storage = Storage::default().with_file_size_limit(104857600); assert_eq!(
storage.env_vars.get("FILE_SIZE_LIMIT"),
Some(&"104857600".to_string())
);
}
#[test]
fn test_with_file_storage_path() {
let storage = Storage::default().with_file_storage_path("/data/storage");
assert_eq!(
storage.env_vars.get("FILE_STORAGE_BACKEND_PATH"),
Some(&"/data/storage".to_string())
);
}
#[test]
fn test_with_upload_signed_url_expiration() {
let storage = Storage::default().with_upload_signed_url_expiration(3600);
assert_eq!(
storage.env_vars.get("UPLOAD_SIGNED_URL_EXPIRATION_TIME"),
Some(&"3600".to_string())
);
}
#[test]
fn test_with_multitenant() {
let storage = Storage::default().with_multitenant(true);
assert_eq!(
storage.env_vars.get("IS_MULTITENANT"),
Some(&"true".to_string())
);
}
#[test]
fn test_with_tus_url_path() {
let storage = Storage::default().with_tus_url_path("/upload/resumable");
assert_eq!(
storage.env_vars.get("TUS_URL_PATH"),
Some(&"/upload/resumable".to_string())
);
}
#[test]
fn test_with_tag_overrides_default() {
let storage = Storage::default().with_tag("v1.0.0");
assert_eq!(storage.tag(), "v1.0.0");
}
#[test]
fn test_with_env_adds_custom_variable() {
let storage = Storage::default()
.with_env("CUSTOM_VAR", "custom_value")
.with_env("ANOTHER_VAR", "another_value");
assert_eq!(
storage.env_vars.get("CUSTOM_VAR"),
Some(&"custom_value".to_string())
);
assert_eq!(
storage.env_vars.get("ANOTHER_VAR"),
Some(&"another_value".to_string())
);
}
#[test]
fn test_builder_method_chaining() {
let storage = Storage::default()
.with_database_url("postgres://user:pass@localhost:5432/db")
.with_anon_key("anon-key")
.with_service_key("service-key")
.with_storage_backend("s3")
.with_region("us-west-2")
.with_global_s3_bucket("my-bucket")
.with_file_size_limit(200000000)
.with_tag("v1.0.0");
assert_eq!(
storage.env_vars.get("DATABASE_URL"),
Some(&"postgres://user:pass@localhost:5432/db".to_string())
);
assert_eq!(
storage.env_vars.get("ANON_KEY"),
Some(&"anon-key".to_string())
);
assert_eq!(
storage.env_vars.get("SERVICE_KEY"),
Some(&"service-key".to_string())
);
assert_eq!(
storage.env_vars.get("STORAGE_BACKEND"),
Some(&"s3".to_string())
);
assert_eq!(
storage.env_vars.get("REGION"),
Some(&"us-west-2".to_string())
);
assert_eq!(
storage.env_vars.get("GLOBAL_S3_BUCKET"),
Some(&"my-bucket".to_string())
);
assert_eq!(
storage.env_vars.get("FILE_SIZE_LIMIT"),
Some(&"200000000".to_string())
);
assert_eq!(storage.tag(), "v1.0.0");
}
#[test]
fn test_new_creates_default_instance() {
let storage = Storage::new();
assert_eq!(storage.name(), NAME);
assert_eq!(storage.tag(), TAG);
}
#[test]
fn test_expose_ports() {
let storage = Storage::default();
let ports = storage.expose_ports();
assert_eq!(ports.len(), 1);
assert_eq!(ports[0], ContainerPort::Tcp(5000));
}
#[test]
fn test_ready_conditions() {
let storage = Storage::default();
let conditions = storage.ready_conditions();
assert_eq!(conditions.len(), 1);
}
}