#!/bin/bash set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" TEST_DIR="${SCRIPT_DIR}/test_tmp" TEST_TOKEN="test-token-$(date +%s)" CONTAINER_NAME="sos-test-$(date +%s)" TEST_PORT="" # Will be set dynamically after container starts CLEANUP_NEEDED=false # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color function log_info() { echo -e "${GREEN}[INFO]${NC} $1" } function log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } function log_warning() { echo -e "${YELLOW}[WARN]${NC} $1" } function cleanup() { if [ "$CLEANUP_NEEDED" = true ]; then log_info "Cleaning up test environment..." # Stop and remove container if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true log_info "Removed test container" fi # Clean up test directory if [ -d "${TEST_DIR}" ]; then rm -rf "${TEST_DIR}" log_info "Removed test directory" fi fi } trap cleanup EXIT function die() { log_error "$@" exit 1 } function check_dependencies() { log_info "Checking dependencies..." # Check for Docker if ! command -v docker &> /dev/null; then die "Docker is required but not installed" fi # Check for jq if ! command -v jq &> /dev/null; then die "jq is required but not installed" fi # Check for curl if ! command -v curl &> /dev/null; then die "curl is required but not installed" fi # Check Docker daemon is running if ! docker info >/dev/null 2>&1; then die "Docker daemon is not running" fi log_info "All dependencies satisfied" } function setup_test_environment() { log_info "Setting up test environment..." CLEANUP_NEEDED=true # Create test directory structure rm -rf "${TEST_DIR}" mkdir -p "${TEST_DIR}/test_files" mkdir -p "${TEST_DIR}/config" # Generate hashed token log_info "Generating authentication token..." HASHED_TOKEN=$(docker run --rm gitea.jde.nz/public/simple-object-server /sos/hash_token "${TEST_TOKEN}" 2>/dev/null | grep '^\$2[aby]\$' | head -1) if [ -z "${HASHED_TOKEN}" ]; then die "Failed to generate hashed token" fi # Create test configuration in config directory cat > "${TEST_DIR}/config/sos_config.json" < "${TEST_DIR}/test_files/test1.txt" echo "This is test file 2 with more content" > "${TEST_DIR}/test_files/test2.txt" dd if=/dev/urandom of="${TEST_DIR}/test_files/binary_test.bin" bs=1024 count=10 2>/dev/null # Create the sos binary for testing cp "${SCRIPT_DIR}/sos" "${TEST_DIR}/test_files/sos_binary" log_info "Created test files" } function start_test_server() { log_info "Starting SOS test server..." # Ensure the config file exists and is readable if [ ! -f "${TEST_DIR}/config/sos_config.json" ]; then die "Config file ${TEST_DIR}/config/sos_config.json does not exist" fi # Start the container without specifying host port (Docker will assign one) # No volume mounts - will use docker cp/exec for file operations local container_id=$(docker run -d \ --name "${CONTAINER_NAME}" \ -p 80 \ gitea.jde.nz/public/simple-object-server 2>&1) if [ $? -ne 0 ]; then log_error "Failed to start container: ${container_id}" die "Failed to start test server" fi log_info "Container started with ID: ${container_id:0:12}" # Get the dynamically assigned port TEST_PORT=$(docker port "${CONTAINER_NAME}" 80 | cut -d: -f2) if [ -z "${TEST_PORT}" ]; then log_error "Failed to get container port" die "Failed to get container port mapping" fi log_info "Container mapped to port: ${TEST_PORT}" # Copy the config file into the container using docker cp log_info "Copying config file into container..." docker cp "${TEST_DIR}/config/sos_config.json" "${CONTAINER_NAME}:/data/sos_config.json" if [ $? -ne 0 ]; then log_error "Failed to copy config file into container" docker logs "${CONTAINER_NAME}" 2>&1 die "Failed to copy config file" fi # Ensure storage directory exists in container docker exec "${CONTAINER_NAME}" mkdir -p /data/storage if [ $? -ne 0 ]; then log_warning "Could not create storage directory (may already exist)" fi # Restart the container to pick up the config file log_info "Restarting container to load config..." docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 if [ $? -ne 0 ]; then log_error "Failed to restart container" die "Failed to restart container" fi # Give container more time to initialize after restart sleep 5 # Verify port mapping after restart (it might change) local new_port=$(docker port "${CONTAINER_NAME}" 80 | cut -d: -f2) if [ -z "${new_port}" ]; then log_error "Lost port mapping after restart" docker logs "${CONTAINER_NAME}" 2>&1 | tail -20 die "Container port mapping lost after restart" fi if [ "${new_port}" != "${TEST_PORT}" ]; then log_info "Port changed after restart: ${TEST_PORT} -> ${new_port}" TEST_PORT="${new_port}" fi # Check if container is still running if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then log_error "Container stopped unexpectedly. Logs:" docker logs "${CONTAINER_NAME}" 2>&1 # Additional debugging for CI environment log_error "Debug: Checking /data directory in container:" docker exec "${CONTAINER_NAME}" ls -la /data/ 2>&1 || true log_error "Debug: Config file in container:" docker exec "${CONTAINER_NAME}" cat /data/sos_config.json 2>&1 || true die "Container failed to stay running" fi # Wait for server to be ready log_info "Waiting for server to be ready..." local max_attempts=60 local attempt=0 while [ $attempt -lt $max_attempts ]; do # Try to check if the server responds - use a simple curl with timeout # The SOS server should respond to a basic GET request local http_code=$(curl -s --max-time 2 -o /dev/null -w "%{http_code}" "http://localhost:${TEST_PORT}/" 2>/dev/null || echo "000") # Debug: Show HTTP code and container status on first few attempts if [ $attempt -lt 3 ]; then log_info "Health check attempt $((attempt + 1)): HTTP code ${http_code}" # Check if container is actually running if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then log_info "Container is running, checking logs..." docker logs "${CONTAINER_NAME}" 2>&1 | tail -5 else log_error "Container is not running!" fi fi if [ "$http_code" = "200" ] || [ "$http_code" = "204" ] || [ "$http_code" = "404" ]; then log_info "Server is ready! (HTTP ${http_code})" return 0 fi # Check container is still running every 5 attempts if [ $((attempt % 5)) -eq 0 ]; then if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then log_error "Container stopped while waiting. Logs:" docker logs "${CONTAINER_NAME}" 2>&1 | tail -20 die "Container stopped unexpectedly" fi # Show progress every 5 seconds if [ $attempt -gt 0 ]; then log_info "Still waiting for server... (${attempt}/${max_attempts})" fi fi sleep 1 attempt=$((attempt + 1)) done log_error "Server did not become ready. Container logs:" docker logs "${CONTAINER_NAME}" 2>&1 | tail -20 die "Server failed to start after ${max_attempts} seconds" } function test_upload() { local file="$1" local label="$2" shift 2 local extra_labels="$@" log_info "Testing upload: ${file} with label ${label}" export SOS_WRITE_TOKEN="${TEST_TOKEN}" # Override to use HTTP for local testing export SOS_TEST_MODE=1 "${SCRIPT_DIR}/sos" upload "localhost:${TEST_PORT}" "${file}" "${label}" ${extra_labels} 2>&1 | tee "${TEST_DIR}/upload_output.txt" if [ ${PIPESTATUS[0]} -ne 0 ]; then log_error "Upload failed for ${file}" return 1 fi # Verify upload was successful if grep -q "Download URL:" "${TEST_DIR}/upload_output.txt"; then log_info "Upload successful for ${file}" return 0 else log_error "Upload verification failed for ${file}" return 1 fi } function test_retrieval() { local identifier="$1" local expected_content="$2" log_info "Testing retrieval by ${identifier}" # Try to retrieve the file local response=$(curl -s "http://localhost:${TEST_PORT}/${identifier}") if [ -z "${response}" ]; then log_error "Failed to retrieve by ${identifier}" return 1 fi # For hash retrieval, test the actual hash endpoint if [[ "${identifier}" =~ ^[a-f0-9]{64}$ ]]; then local meta_response=$(curl -s "http://localhost:${TEST_PORT}/meta/${identifier}") if echo "${meta_response}" | jq -e '.metadata' >/dev/null 2>&1; then log_info "Successfully retrieved metadata for hash ${identifier}" else log_error "Failed to retrieve metadata for hash ${identifier}" return 1 fi fi log_info "Retrieval successful for ${identifier}" return 0 } function test_duplicate_upload() { log_info "Testing duplicate file upload (deduplication)..." # Create a duplicate file with different name cp "${TEST_DIR}/test_files/test1.txt" "${TEST_DIR}/test_files/test1_dup.txt" export SOS_WRITE_TOKEN="${TEST_TOKEN}" export SOS_TEST_MODE=1 # Upload original "${SCRIPT_DIR}/sos" upload "localhost:${TEST_PORT}" "${TEST_DIR}/test_files/test1.txt" "dup:original" >/dev/null 2>&1 # Upload duplicate (should detect existing file) local output=$("${SCRIPT_DIR}/sos" upload "localhost:${TEST_PORT}" "${TEST_DIR}/test_files/test1_dup.txt" "dup:copy" 2>&1) if echo "${output}" | grep -q "File already exists, skipping upload"; then log_info "Deduplication working correctly" return 0 else log_error "Deduplication test failed" return 1 fi } function test_metadata_update() { log_info "Testing metadata update..." export SOS_WRITE_TOKEN="${TEST_TOKEN}" export SOS_TEST_MODE=1 # Upload a file "${SCRIPT_DIR}/sos" upload "localhost:${TEST_PORT}" "${TEST_DIR}/test_files/test2.txt" "meta:v1" >/dev/null 2>&1 # Upload same file with different metadata local output=$("${SCRIPT_DIR}/sos" upload "localhost:${TEST_PORT}" "${TEST_DIR}/test_files/test2.txt" "meta:v2" "meta:updated" 2>&1) if echo "${output}" | grep -q "File already exists"; then log_info "Metadata update successful" return 0 else log_error "Metadata update test failed" return 1 fi } function test_exists_endpoint() { log_info "Testing exists endpoint..." # Get hash of an uploaded file local hash=$(curl -s "http://localhost:${TEST_PORT}/hash/test:file1" | jq -r '.hash') if [ -z "${hash}" ] || [ "${hash}" = "null" ]; then log_error "Failed to get hash for exists test" return 1 fi # Test exists endpoint local exists=$(curl -s "http://localhost:${TEST_PORT}/exists/${hash}" | jq -r '.exists') if [ "${exists}" = "true" ]; then log_info "Exists endpoint working correctly" return 0 else log_error "Exists endpoint test failed" return 1 fi } function test_invalid_auth() { log_info "Testing invalid authentication..." # Create a unique test file for this test echo "auth test content" > "${TEST_DIR}/test_files/auth_test.txt" export SOS_WRITE_TOKEN="invalid-token" export SOS_TEST_MODE=1 # Run upload with invalid token and capture output + exit code set +e # Temporarily disable exit on error "${SCRIPT_DIR}/sos" upload "localhost:${TEST_PORT}" "${TEST_DIR}/test_files/auth_test.txt" "auth:test" 2>&1 | tee "${TEST_DIR}/auth_test_output.txt" local exit_code=${PIPESTATUS[0]} set -e # Re-enable exit on error local output=$(cat "${TEST_DIR}/auth_test_output.txt") # Check if it failed with non-zero exit code and error message if [ ${exit_code} -ne 0 ] && (echo "${output}" | grep -q "Failed to upload" || echo "${output}" | grep -q "Invalid write token"); then log_info "Invalid auth correctly rejected" return 0 else log_error "Invalid auth test failed - upload should have been rejected (exit code: ${exit_code})" echo "Output was: ${output}" return 1 fi } function run_tests() { local total_tests=0 local passed_tests=0 # Disable exit on error for test functions set +e log_info "Starting test suite..." echo "" # Test 1: Basic upload ((total_tests++)) test_upload "${TEST_DIR}/test_files/test1.txt" "test:file1" if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 2: Upload with multiple labels ((total_tests++)) test_upload "${TEST_DIR}/test_files/test2.txt" "test:file2" "version:1.0" "env:test" if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 3: Binary file upload ((total_tests++)) test_upload "${TEST_DIR}/test_files/binary_test.bin" "binary:test" if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 4: Upload sos binary itself ((total_tests++)) test_upload "${TEST_DIR}/test_files/sos_binary" "sos:test" "sos:latest" if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 5: Retrieval by label ((total_tests++)) test_retrieval "test:file1" "This is test file 1" if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 6: Deduplication ((total_tests++)) test_duplicate_upload if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 7: Metadata update ((total_tests++)) test_metadata_update if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 8: Exists endpoint ((total_tests++)) test_exists_endpoint if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Test 9: Invalid authentication ((total_tests++)) test_invalid_auth if [ $? -eq 0 ]; then ((passed_tests++)) fi echo "" # Re-enable exit on error set -e # Summary echo "==================================" if [ ${passed_tests} -eq ${total_tests} ]; then echo -e "${GREEN}All tests passed!${NC} (${passed_tests}/${total_tests})" return 0 else echo -e "${RED}Some tests failed!${NC} (${passed_tests}/${total_tests})" return 1 fi } function main() { echo "SOS Test Suite" echo "==============" echo "" check_dependencies setup_test_environment start_test_server echo "" run_tests local test_result=$? echo "" log_info "Test suite completed" # Show container logs if tests failed if [ ${test_result} -ne 0 ]; then log_warning "Showing server logs for debugging:" docker logs "${CONTAINER_NAME}" 2>&1 | tail -20 fi exit ${test_result} } main "$@"