Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,540 changes: 875 additions & 665 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,21 @@ async-compression = { version = "0.4.37", features = ["tokio", "gzip"] }
tokio-tar = "0.3.1"
oauth2 = "5.0.0"
hyper = "1.8.1"
async-http-client = "0.2.0"
aes-gcm = "0.11.0-rc.3"
generic-array = "0.14.7"
futures-util = "0.3.31"
tokio-stream = "0.1.18"
aes = "0.9.0-rc.4"
typenum = "1.19.0"
testcontainers = "0.27.1"
testcontainers-modules = { version = "0.15.0", features = ["postgres"] }
postgres = "0.19.12"

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
mockall = "0.13"
testcontainers = "0.27.1"
wiremock = "0.6"


[[bin]]
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
include .env
export $(shell sed 's/=.*//' .env)
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh

.PHONY: seed-mongo seed-mysql seed-postgres

up:
@bash $(CLUSTER_SCRIPT)

seed-mongo:
@echo "Seeding MongoDB..."
bash ./scripts/mongo/seed-mongo.sh
Expand Down
7 changes: 3 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ services:
# - ./databases.toml:/config/config.toml
- cargo-registry:/usr/local/cargo/registry
- cargo-git:/usr/local/cargo/git
# - cargo-target:/app/target
- cargo-target:/app/target
# - sqlite-data:/sqlite-data/workspace/data
# - ./scripts/sqlite/test-db:/sqlite-data-2/workspace/data
environment:
APP_ENV: development
LOG: debug
TZ: "Europe/Paris"
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNjI1MDQzY2YtN2MwMC00M2M4LWJjYzktZDM1MTk5ODk2ZGNkIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiOGYwMmExZTAtNDY0NC00MWFmLWIzYjctYjZkYWNjNzQ4OWVhIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
#POOLING: 1
#DATABASES_CONFIG_FILE: "config.toml"
extra_hosts:
Expand Down Expand Up @@ -107,7 +107,7 @@ services:
volumes:
cargo-registry:
cargo-git:
# cargo-target:
cargo-target:

postgres-data:
# mariadb-data:
Expand All @@ -120,4 +120,3 @@ networks:
name: portabase_network
external: true

# docker network create portabase_network
39 changes: 39 additions & 0 deletions docker/entrypoints/app-dev-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
set -euo pipefail

check_docker() {
if ! docker info > /dev/null 2>&1; then
echo "Docker is not running. Attempting to start Docker..."
if [[ "$OSTYPE" == "darwin"* ]]; then
open -a Docker
echo "Waiting for Docker to start..."
until docker info > /dev/null 2>&1; do
sleep 2
done
elif command -v systemctl >/dev/null 2>&1; then
sudo systemctl start docker
else
echo "Cannot start Docker automatically. Please start Docker manually."
exit 1
fi
else
echo "Docker is running."
fi
}

check_network() {
local network_name="portabase_network"
if ! docker network ls --format '{{.Name}}' | grep -q "^${network_name}$"; then
echo "Docker network '${network_name}' not found. Creating..."
docker network create "${network_name}"
else
echo "Docker network '${network_name}' already exists."
fi
}

check_docker
check_network

echo "Starting docker-compose..."
docker-compose -f ./docker-compose.yml up
echo "Docker-compose started successfully."
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod services;
mod settings;
mod tasks;
mod utils;
#[cfg(test)]
mod tests;

use crate::tasks::ping::ping_server;
use crate::utils::locks::FileLock;
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod postgres;
35 changes: 35 additions & 0 deletions src/tests/domain/postgres.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use oauth2::url;
use testcontainers::runners::AsyncRunner;
use testcontainers_modules::postgres::Postgres;
use crate::services::config::{DatabaseConfig, DbType};
use url::Host;
use crate::domain::factory::DatabaseFactory;

#[tokio::test]
async fn postgres_ping_test() {

let container = Postgres::default()
.start()
.await
.unwrap();

let host = container.get_host().await.unwrap_or(Host::parse("127.0.0.1").unwrap());
let port = container.get_host_port_ipv4(5432).await.unwrap_or(5432) ;

let config = DatabaseConfig {
name: "My test Postgres Database".to_string(),
database: "postgres".to_string(),
db_type: DbType::Postgresql,
username: "postgres".to_string(),
password: "postgres".to_string(),
port,
host: host.to_string(),
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
};

let db = DatabaseFactory::create_for_backup(config.clone()).await;
let reachable = db.ping().await.unwrap_or_else(|_| false);

assert_eq!(reachable, true);
}
2 changes: 2 additions & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod utils;
mod domain;
46 changes: 46 additions & 0 deletions src/tests/utils/common_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use serde_json::json;
use crate::utils::common::{vec_to_option_json, BackupMethod};

#[test]
fn backup_method_to_string_automatic() {
let method = BackupMethod::Automatic;
assert_eq!(method.to_string(), "automatic");
}

#[test]
fn backup_method_to_string_manual() {
let method = BackupMethod::Manual;
assert_eq!(method.to_string(), "manual");
}

#[test]
fn vec_to_option_json_returns_none_when_empty() {
let v: Vec<i32> = vec![];
let result = vec_to_option_json(v);

assert!(result.is_none());
}

#[test]
fn vec_to_option_json_serializes_vector() {
let v = vec![1, 2, 3];
let result = vec_to_option_json(v);

assert_eq!(result, Some(json!([1, 2, 3])));
}

#[test]
fn vec_to_option_json_serializes_struct_vector() {
#[derive(serde::Serialize)]
struct Item {
id: u32,
}

let v = vec![Item { id: 1 }, Item { id: 2 }];
let result = vec_to_option_json(v);

assert_eq!(result, Some(json!([
{ "id": 1 },
{ "id": 2 }
])));
}
73 changes: 73 additions & 0 deletions src/tests/utils/compress_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use tempfile::tempdir;
use tokio::fs::{write, read};
use anyhow::Result;
use crate::utils::compress::{compress_to_tar_gz_large, decompress_large_tar_gz};

#[tokio::test]
async fn compress_creates_tar_gz() -> Result<()> {
let tmp = tempdir()?;
let file_path = tmp.path().join("test.txt");
write(&file_path, b"hello world").await?;

let result = compress_to_tar_gz_large(&file_path).await?;
assert!(result.compressed_path.exists());
assert_eq!(result.compressed_path.extension().unwrap(), "gz");

Ok(())
}

#[tokio::test]
async fn compress_skips_existing_tar_gz() -> Result<()> {
let tmp = tempdir()?;
let file_path = tmp.path().join("already.tar.gz");
write(&file_path, b"compressed").await?;

let result = compress_to_tar_gz_large(&file_path).await?;
// Should return same path without creating a new file
assert_eq!(result.compressed_path, file_path);

Ok(())
}

#[tokio::test]
async fn decompress_restores_file() -> Result<()> {
let tmp = tempdir()?;
let file_path = tmp.path().join("file.txt");
write(&file_path, b"data for decompress").await?;

let compress_result = compress_to_tar_gz_large(&file_path).await?;
let output_dir = tmp.path().join("out");
tokio::fs::create_dir_all(&output_dir).await?;

let extracted_files = decompress_large_tar_gz(&compress_result.compressed_path, &output_dir).await?;
assert_eq!(extracted_files.len(), 1);

let extracted_content = read(&extracted_files[0]).await?;
assert_eq!(extracted_content, b"data for decompress");

Ok(())
}

#[tokio::test]
async fn decompress_multiple_files() -> Result<()> {
let tmp = tempdir()?;
let file1 = tmp.path().join("file1.txt");
let file2 = tmp.path().join("file2.txt");
write(&file1, b"file1").await?;
write(&file2, b"file2").await?;

// Compress both files individually (for simplicity in this test)
let compress1 = compress_to_tar_gz_large(&file1).await?;
let compress2 = compress_to_tar_gz_large(&file2).await?;

let output_dir = tmp.path().join("out_multi");
tokio::fs::create_dir_all(&output_dir).await?;

let extracted1 = decompress_large_tar_gz(&compress1.compressed_path, &output_dir).await?;
let extracted2 = decompress_large_tar_gz(&compress2.compressed_path, &output_dir).await?;

assert_eq!(extracted1.len(), 1);
assert_eq!(extracted2.len(), 1);

Ok(())
}
104 changes: 104 additions & 0 deletions src/tests/utils/deserializer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#[cfg(test)]
mod tests {
use serde::Deserialize;
use toml::map::Map;
use toml::Value;
use crate::utils::deserializer::{camel_to_snake, deserialize_snake_case, to_snake_case};

#[test]
fn camel_to_snake_simple() {
assert_eq!(camel_to_snake("CamelCase"), "camel_case");
assert_eq!(camel_to_snake("simpleTest"), "simple_test");
assert_eq!(camel_to_snake("already_snake"), "already_snake");
assert_eq!(camel_to_snake("X"), "x");
assert_eq!(camel_to_snake("ABTest"), "a_b_test");
}

#[test]
fn to_snake_case_nested_table() {
let mut inner_table = Map::new();
inner_table.insert("InnerKey".into(), Value::String("value".into()));

let mut outer_table = Map::new();
outer_table.insert("OuterKey".into(), Value::Table(inner_table));

let value = Value::Table(outer_table);

// Expected snake_case
let mut expected_inner = Map::new();
expected_inner.insert("inner_key".into(), Value::String("value".into()));

let mut expected_outer = Map::new();
expected_outer.insert("outer_key".into(), Value::Table(expected_inner));

let expected = Value::Table(expected_outer);

let result = to_snake_case(value);
assert_eq!(result, expected);
}

#[test]
fn to_snake_case_array_of_tables() {
let mut table1 = Map::new();
table1.insert("CamelKey".into(), Value::Integer(1));

let mut table2 = Map::new();
table2.insert("AnotherKey".into(), Value::Integer(2));

let value = Value::Array(vec![
Value::Table(table1),
Value::Table(table2),
]);

let mut expected_table1 = Map::new();
expected_table1.insert("camel_key".into(), Value::Integer(1));

let mut expected_table2 = Map::new();
expected_table2.insert("another_key".into(), Value::Integer(2));

let expected = Value::Array(vec![
Value::Table(expected_table1),
Value::Table(expected_table2),
]);

let result = to_snake_case(value);
assert_eq!(result, expected);
}

#[test]
fn deserialize_snake_case_works_with_struct() {
#[derive(Deserialize, Debug, PartialEq)]
struct Config {
some_value: i32,
nested_table: Nested,
}

#[derive(Deserialize, Debug, PartialEq)]
struct Nested {
inner_value: String,
}

let toml_str = r#"
SomeValue = 42

[NestedTable]
InnerValue = "hello"
"#;

let value: Value = toml::from_str(toml_str).unwrap();
let snake_value = deserialize_snake_case(value).unwrap();

// Deserialize to struct
let config: Config = snake_value.try_into().unwrap();

assert_eq!(config.some_value, 42);
assert_eq!(config.nested_table.inner_value, "hello");
}

#[test]
fn to_snake_case_non_table_value() {
let value = Value::String("unchanged".into());
let result = to_snake_case(value.clone());
assert_eq!(result, value);
}
}
Loading
Loading