mirror of
https://github.com/Portabase/agent.git
synced 2026-09-10 01:57:10 +00:00
feat: add test for util functions and start working on database providers tests procedures
This commit is contained in:
Generated
+878
-668
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -120,4 +120,3 @@ networks:
|
||||
name: portabase_network
|
||||
external: true
|
||||
|
||||
# docker network create portabase_network
|
||||
Executable
+39
@@ -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."
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
mod postgres;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod utils;
|
||||
mod domain;
|
||||
@@ -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 }
|
||||
])));
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde_json::json;
|
||||
use crate::utils::edge_key::{decode_edge_key, EdgeKeyError};
|
||||
|
||||
#[test]
|
||||
fn decode_valid_edge_key() {
|
||||
let edge_key_b64 = "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNjI1MDQzY2YtN2MwMC00M2M4LWJjYzktZDM1MTk5ODk2ZGNkIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ==";
|
||||
let decoded = decode_edge_key(edge_key_b64).unwrap();
|
||||
|
||||
assert_eq!(decoded.server_url, "http://localhost:8887");
|
||||
assert_eq!(decoded.agent_id, "625043cf-7c00-43c8-bcc9-d35199896dcd");
|
||||
assert_eq!(decoded.master_key_b64, "BXV3XolC656SV7dNgcWPGQlk++rpLI6lGDi7CPB5ieo=");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_edge_key_missing_field() {
|
||||
let incomplete_json = json!({
|
||||
"serverUrl": "http://localhost:8887",
|
||||
"agentId": "123"
|
||||
// masterKeyB64 is missing
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let b64 = general_purpose::URL_SAFE.encode(incomplete_json);
|
||||
let result = decode_edge_key(&b64);
|
||||
|
||||
match result {
|
||||
Err(EdgeKeyError::InvalidKey) => {}
|
||||
_ => panic!("Expected InvalidKey error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_edge_key_invalid_base64() {
|
||||
let invalid_b64 = "!!!notbase64!!!";
|
||||
let result = decode_edge_key(invalid_b64);
|
||||
|
||||
match result {
|
||||
Err(EdgeKeyError::Base64Error(_)) => {}
|
||||
_ => panic!("Expected Base64Error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_edge_key_invalid_json() {
|
||||
let invalid_json_b64 = general_purpose::URL_SAFE.encode("not a json string");
|
||||
let result = decode_edge_key(&invalid_json_b64);
|
||||
|
||||
match result {
|
||||
Err(EdgeKeyError::JsonError(_)) => {}
|
||||
_ => panic!("Expected JsonError"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod normalize_cron_tests;
|
||||
mod common_tests;
|
||||
mod compress_tests;
|
||||
mod deserializer;
|
||||
mod edge_key_tests;
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::utils::text::normalize_cron;
|
||||
use cron::Schedule;
|
||||
use std::str::FromStr;
|
||||
use crate::utils::task_manager::cron::next_run_timestamp;
|
||||
|
||||
#[test]
|
||||
fn normalize_adds_seconds_to_five_field_cron() {
|
||||
let input = "*/5 * * * *";
|
||||
let normalized = normalize_cron(input);
|
||||
|
||||
assert_eq!(normalized, "0 */5 * * * *");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_keeps_six_field_cron() {
|
||||
let input = "0 */5 * * * *";
|
||||
let normalized = normalize_cron(input);
|
||||
|
||||
assert_eq!(normalized, "0 */5 * * * *");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalized_expression_is_valid_for_cron_schedule() {
|
||||
let input = "*/5 * * * *";
|
||||
let normalized = normalize_cron(input);
|
||||
|
||||
let schedule = Schedule::from_str(&normalized);
|
||||
assert!(schedule.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_run_timestamp_returns_future_timestamp() {
|
||||
let expr = normalize_cron("*/1 * * * *");
|
||||
let ts = next_run_timestamp(&expr);
|
||||
|
||||
let now = chrono::Local::now().timestamp();
|
||||
assert!(ts > now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalization_does_not_break_schedule_parsing() {
|
||||
let input = "0 */10 * * * *";
|
||||
let normalized = normalize_cron(input);
|
||||
|
||||
let schedule = Schedule::from_str(&normalized).unwrap();
|
||||
let next = schedule.upcoming(chrono::Local).next();
|
||||
|
||||
assert!(next.is_some());
|
||||
}
|
||||
@@ -90,7 +90,6 @@ pub async fn decompress_large_tar_gz(
|
||||
extracted_files.push(full_path);
|
||||
}
|
||||
|
||||
// remove_file(tar_gz_path).await?;
|
||||
info!("Decompressed {:?} into {:?}", tar_gz_path, output_dir);
|
||||
|
||||
Ok(extracted_files)
|
||||
|
||||
@@ -8,7 +8,8 @@ where
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
Ok(to_snake_case(value))
|
||||
}
|
||||
fn to_snake_case(value: Value) -> Value {
|
||||
|
||||
pub fn to_snake_case(value: Value) -> Value {
|
||||
match value {
|
||||
Value::Table(table) => Value::Table(
|
||||
table
|
||||
@@ -16,14 +17,12 @@ fn to_snake_case(value: Value) -> Value {
|
||||
.map(|(k, v)| (camel_to_snake(&k), to_snake_case(v)))
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(arr) => {
|
||||
Value::Array(arr.into_iter().map(to_snake_case).collect())
|
||||
}
|
||||
Value::Array(arr) => Value::Array(arr.into_iter().map(to_snake_case).collect()),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn camel_to_snake(s: &str) -> String {
|
||||
pub fn camel_to_snake(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for (i, c) in s.chars().enumerate() {
|
||||
if c.is_uppercase() {
|
||||
|
||||
Reference in New Issue
Block a user