diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..5a5be18 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,168 @@ +# Architecture Overview + +This document provides a detailed technical overview of the getpkg project and its associated tools. + +## Project Structure + +The repository contains multiple tools in the dropshell ecosystem: + +- **getpkg** - The main C++ package manager +- **sos** - Simple object storage upload utility +- **whatsdirty** - Git repository status checker for subdirectories +- **dehydrate** - File to C++ code generator +- **bb64** - Base64 encoder/decoder +- **gp** - Git push utility + +## getpkg Architecture + +### Core Components + +The main getpkg application is built in C++ using modern C++23 standards with the following key components: + +#### Package Management +- **`GetbinClient`** (`src/GetbinClient.{hpp,cpp}`) + - HTTP client with multi-server support for downloading/uploading packages + - Implements fallback logic for server failures + - Progress callback support for downloads/uploads + - Server-specific and multi-server operations + +- **`PackageMetadata`** (`src/PackageMetadata.{hpp,cpp}`) + - Enhanced metadata structure with server source tracking + - Supports migration from legacy single-server format + - Stores: name, version, hash, architecture, source server, install date + - JSON serialization/deserialization + - Validation methods for all fields + +- **`PackageMetadataManager`** (`src/PackageMetadata.{hpp,cpp}`) + - Manages package metadata directory structure + - Handles legacy format migration + - Package enumeration and validation + +#### Server Management +- **`ServerManager`** (`src/ServerManager.{hpp,cpp}`) + - Manages multiple package servers with write tokens + - Server configuration persistence + - Token management for publishing + - Server reachability validation + +#### System Integration +- **`BashrcEditor`** (`src/BashrcEditor.{hpp,cpp}`) + - Manages `~/.bashrc_getpkg` file modifications + - Handles PATH updates and bash completions + - Safe file editing with atomic operations + +- **`DropshellScriptManager`** (`src/DropshellScriptManager.{hpp,cpp}`) + - Manages tool installation and configuration + - Handles setup scripts execution + - Directory structure management + +- **`MigrationManager`** (`src/MigrationManager.{hpp,cpp}`) + - Handles migrations between getpkg versions + - Legacy format conversions + - Configuration updates + +### Common Utilities +Located in `src/common/`: +- **`archive_tgz`** - TAR.GZ archive creation/extraction +- **`hash`** - File hashing utilities +- **`output`** - Formatted console output +- **`temp_directory`** - Temporary directory management +- **`xxhash`** - Fast hashing algorithm implementation + +## Build System + +### Docker-Based Build +- Uses containerized build environment: `gitea.jde.nz/public/dropshell-build-base:latest` +- Ensures consistent builds across different host systems +- Static linking for maximum portability + +### CMake Configuration +- C++23 standard required +- Static executable building (`-static` linker flags) +- External dependencies: + - nlohmann_json - JSON parsing + - CPRStatic - HTTP client library +- Version format: `YYYY.MMDD.HHMM` (timestamp-based) + +### Build Scripts +- **`build.sh`** - Individual tool build script +- **`test.sh`** - Run tests for individual tools +- **`publish.sh`** - Publish tool to getpkg.xyz (requires SOS_WRITE_TOKEN) +- **`buildtestpublish_all.sh`** - Master script that builds, tests, and publishes all tools + +## File Locations and Structure + +### User Installation +- **Tool installations**: `~/.getpkg//` +- **Executable symlinks**: `~/.local/bin/getpkg/` +- **Configuration**: `~/.config/getpkg/` + - `packages/.json` - Package metadata + - `servers.json` - Server configuration +- **Bash integration**: `~/.bashrc_getpkg` (sourced by ~/.bashrc) + +### Repository Structure +``` +getpkg/ +├── getpkg/ # Main package manager +│ ├── src/ # Source code +│ ├── test/ # Test suite +│ └── build.sh # Build script +├── sos/ # Simple object storage +├── whatsdirty/ # Git status checker +├── dehydrate/ # File to C++ converter +├── bb64/ # Base64 utility +├── gp/ # Git push utility +└── buildtestpublish_all.sh # Master build script +``` + +## Multi-Server Architecture + +getpkg supports multiple package servers with intelligent fallback: + +1. **Server Configuration** + - Multiple servers can be configured + - Each server can have an optional write token + - First server with token becomes default publish target + +2. **Download Strategy** + - Attempts servers in configured order + - Falls back to next server on failure + - Tracks which server provided each package + +3. **Publishing** + - Requires SOS_WRITE_TOKEN environment variable + - Publishes to first server with valid token + - Supports architecture-specific uploads + +## Security Considerations + +- No root access required - installs to user home directory +- Static linking prevents dependency attacks +- Hash verification for downloaded packages +- Token-based authentication for publishing + +## Testing + +- Docker-based test environment for consistency +- Integration tests for package operations +- Unit tests for individual components +- Test artifacts isolated in `test_temp/` directory + +## Development Workflow + +1. **Local Development** + ```bash + cd getpkg && ./build.sh # Build + cd getpkg && ./test.sh # Test + ``` + +2. **Full Build** + ```bash + ./buildtestpublish_all.sh # Build all tools + ``` + +3. **Publishing** (requires SOS_WRITE_TOKEN) + ```bash + export SOS_WRITE_TOKEN="your-token" + cd getpkg && ./publish.sh # Publish single tool + ``` \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d0849fd..2752969 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,19 +6,35 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This repository contains Dropshell Tools - a collection of utilities that support dropshell development. The main tool is `getpkg`, a C++ command-line application that manages tool installation, updates, and publishing for the dropshell ecosystem. +## Repository Structure + +- **getpkg**: Package manager for dropshell tools (C++23) +- **sos**: Simple object storage upload utility (Bash) +- **whatsdirty**: Git repository status checker (Bash) +- **dehydrate**: File to C++ code generator (C++) +- **bb64**: Base64 encoder/decoder (C++) +- **gp**: Git push utility (Bash) + ## Architecture -### Core Components +### Core Components (getpkg) - **getpkg**: Main C++ application (`getpkg/src/`) - `main.cpp`: CLI interface and command routing - - `ArchiveManager`: Handles .tgz archive creation/extraction - - `BashrcEditor`: Manages ~/.bashrc_dropshell_tool script modifications + - `GetbinClient`: Multi-server HTTP client for downloading/uploading tools + - `PackageMetadata`: Enhanced metadata with server tracking and migration support + - `ServerManager`: Manages multiple package servers with write tokens + - `BashrcEditor`: Manages ~/.bashrc_getpkg script modifications - `DropshellScriptManager`: Manages tool installation and configuration - - `GetbinClient`: HTTP client for downloading/uploading tools + - `MigrationManager`: Handles legacy format migrations + - `common/`: Shared utilities (archive_tgz, hash, output, temp_directory) -- **sos**: Simple object storage utility -- **whatsdirty**: Git repository status checker +### Key Features + +- **Multi-server support**: Fallback logic for package downloads +- **Architecture awareness**: Supports x86_64, aarch64, and universal packages +- **Migration support**: Handles upgrades from legacy single-server format +- **Static linking**: All tools built as static binaries for portability ### Build System @@ -59,11 +75,33 @@ export CMAKE_BUILD_TYPE="Release" ## Tool Functionality getpkg manages a tool ecosystem by: -- Installing tools to `~/.local/bin/getpkg//` -- Managing bash completions and aliases via `~/.bashrc_getpkg` -- Storing tool metadata in `~/.config/getpkg/` -- Publishing/downloading tools via getbin.xyz object storage +- Installing tools to `~/.getpkg//` with symlinks in `~/.local/bin/getpkg/` +- Managing bash completions and PATH updates via `~/.bashrc_getpkg` +- Storing tool metadata in `~/.config/getpkg/packages/` (JSON format) +- Supporting multi-server package distribution with fallback +- Publishing/downloading tools via object storage servers (default: getpkg.xyz) + +## File Locations + +- **Tool installations**: `~/.getpkg//` +- **Executable symlinks**: `~/.local/bin/getpkg/` (added to PATH) +- **Package metadata**: `~/.config/getpkg/packages/.json` +- **Server configuration**: `~/.config/getpkg/servers.json` +- **Bash integration**: `~/.bashrc_getpkg` (sourced by ~/.bashrc) ## Publishing Requirements -Publishing requires the `SOS_WRITE_TOKEN` environment variable for authentication to the object storage system. \ No newline at end of file +Publishing requires the `SOS_WRITE_TOKEN` environment variable for authentication to the object storage system. + +## Testing + +- Tests create temporary files in `test_temp/` directory +- Docker-based test environment using same build image +- Run `cleanup_test_packages.sh` to remove orphaned test packages from servers + +## Important Notes + +- All builds use static linking for maximum portability +- Version format is YYYY.MMDD.HHMM (timestamp-based) +- Tools should support `version` and `autocomplete` subcommands +- Architecture-specific builds use suffixes like `:x86_64` or `:aarch64` \ No newline at end of file diff --git a/getpkg/src/main.cpp b/getpkg/src/main.cpp index 5c6b630..83bfb4d 100644 --- a/getpkg/src/main.cpp +++ b/getpkg/src/main.cpp @@ -1195,10 +1195,12 @@ int uninstall_tool(int argc, char* argv[]) { std::string home = get_home(); std::filesystem::path configDir = std::filesystem::path(home) / ".config/getpkg"; std::filesystem::path binDir = std::filesystem::path(home) / ".getpkg" / toolName; - std::filesystem::path toolInfoPath = configDir / (toolName + ".json"); - // Check if tool is installed - if (!std::filesystem::exists(toolInfoPath)) { + // Initialize PackageMetadataManager + PackageMetadataManager packageManager(configDir); + + // Check if tool is installed using PackageMetadataManager + if (!packageManager.packageExists(toolName)) { std::cerr << "Tool " << toolName << " is not installed." << std::endl; return 1; } @@ -1215,10 +1217,8 @@ int uninstall_tool(int argc, char* argv[]) { std::filesystem::remove_all(binDir); } - // Remove tool info file - if (std::filesystem::exists(toolInfoPath)) { - std::filesystem::remove(toolInfoPath); - } + // Remove tool metadata + packageManager.removePackageMetadata(toolName); std::cout << "Uninstalled " << toolName << " successfully." << std::endl; return 0; diff --git a/getpkg/test.sh b/getpkg/test.sh index cb23399..38842d6 100755 --- a/getpkg/test.sh +++ b/getpkg/test.sh @@ -44,8 +44,8 @@ cleanup() { # Remove local test directories rm -rf "$TEST_DIR" - rm -rf ~/.config/getpkg/"${TEST_TOOL_NAME}.json" 2>/dev/null || true - rm -rf ~/.config/getpkg/"${TEST_TOOL_NAME}-noarch.json" 2>/dev/null || true + rm -rf ~/.config/getpkg/packages/"${TEST_TOOL_NAME}.json" 2>/dev/null || true + rm -rf ~/.config/getpkg/packages/"${TEST_TOOL_NAME}-noarch.json" 2>/dev/null || true rm -rf ~/.getpkg/"${TEST_TOOL_NAME}" 2>/dev/null || true rm -rf ~/.getpkg/"${TEST_TOOL_NAME}-noarch" 2>/dev/null || true rm -rf ~/.local/bin/getpkg/"${TEST_TOOL_NAME}" 2>/dev/null || true @@ -295,7 +295,7 @@ if [ -n "${SOS_WRITE_TOKEN:-}" ]; then echo "Publish command failed with no output. Checking for missing dependencies..." ldd "$GETPKG" 2>&1 | grep "not found" || echo "All dependencies found" fi - if [[ "$PUBLISH_OUTPUT" =~ Published! ]] && [[ "$PUBLISH_OUTPUT" =~ URL: ]] && [[ "$PUBLISH_OUTPUT" =~ Hash: ]]; then + if [[ "$PUBLISH_OUTPUT" =~ Published.*! ]] && [[ "$PUBLISH_OUTPUT" =~ URL: ]] && [[ "$PUBLISH_OUTPUT" =~ Hash: ]]; then print_test_result "Publish tool with ARCH to getpkg.xyz" 0 # Extract hash for later cleanup @@ -305,8 +305,20 @@ if [ -n "${SOS_WRITE_TOKEN:-}" ]; then # Test 8: Check if published tool exists echo -e "\nTest 8: Check published tool exists" EXISTS_CHECK=$(curl -s "https://getpkg.xyz/exists/${TEST_TOOL_NAME}:${TEST_ARCH}" 2>/dev/null || echo "error") - if [[ "$EXISTS_CHECK" != "error" ]] && [[ "$EXISTS_CHECK" != "false" ]]; then - print_test_result "Published tool exists on server" 0 + # Parse JSON response to check if exists is true + if [[ "$EXISTS_CHECK" != "error" ]]; then + # Try to parse JSON response + if command -v jq >/dev/null 2>&1; then + EXISTS_VALUE=$(echo "$EXISTS_CHECK" | jq -r '.exists' 2>/dev/null || echo "false") + else + # Fallback: extract exists value using grep/sed + EXISTS_VALUE=$(echo "$EXISTS_CHECK" | grep -o '"exists":[^,}]*' | sed 's/.*:\s*//' | tr -d ' ' || echo "false") + fi + if [[ "$EXISTS_VALUE" == "true" ]]; then + print_test_result "Published tool exists on server" 0 + else + print_test_result "Published tool exists on server" 1 + fi else print_test_result "Published tool exists on server" 1 fi @@ -319,7 +331,7 @@ if [ -n "${SOS_WRITE_TOKEN:-}" ]; then # Test 10: Check installed files echo -e "\nTest 10: Check installed files" - if [ -f ~/.config/getpkg/"${TEST_TOOL_NAME}.json" ] && [ -d ~/.getpkg/"${TEST_TOOL_NAME}" ] && [ -L ~/.local/bin/getpkg/"${TEST_TOOL_NAME}" ]; then + if [ -f ~/.config/getpkg/packages/"${TEST_TOOL_NAME}.json" ] && [ -d ~/.getpkg/"${TEST_TOOL_NAME}" ] && [ -L ~/.local/bin/getpkg/"${TEST_TOOL_NAME}" ]; then print_test_result "Tool files installed correctly" 0 else print_test_result "Tool files installed correctly" 1 @@ -338,7 +350,7 @@ if [ -n "${SOS_WRITE_TOKEN:-}" ]; then # First remove the tool rm -rf ~/.getpkg/"${TEST_TOOL_NAME}" rm -rf ~/.local/bin/getpkg/"${TEST_TOOL_NAME}" - rm -f ~/.config/getpkg/"${TEST_TOOL_NAME}.json" + rm -f ~/.config/getpkg/packages/"${TEST_TOOL_NAME}.json" REINSTALL_OUTPUT=$(timeout 3 "$GETPKG" install "$TEST_TOOL_NAME" 2>&1) || REINSTALL_OUTPUT="" if [[ "$REINSTALL_OUTPUT" =~ Installed\ ${TEST_TOOL_NAME}\ successfully ]] || [[ "$REINSTALL_OUTPUT" =~ ${TEST_TOOL_NAME}\ is\ already\ up\ to\ date ]]; then @@ -394,7 +406,7 @@ EOF chmod +x "${TEST_DIR}/${TEST_TOOL_NOARCH}/${TEST_TOOL_NOARCH}" PUBLISH_NOARCH_OUTPUT=$(timeout 3 "$GETPKG" publish "${TEST_TOOL_NOARCH}" "${TEST_DIR}/${TEST_TOOL_NOARCH}" 2>&1) || PUBLISH_NOARCH_OUTPUT="" - if [[ "$PUBLISH_NOARCH_OUTPUT" =~ Published! ]] && [[ "$PUBLISH_NOARCH_OUTPUT" =~ URL: ]] && [[ "$PUBLISH_NOARCH_OUTPUT" =~ Hash: ]]; then + if [[ "$PUBLISH_NOARCH_OUTPUT" =~ Published.*! ]] && [[ "$PUBLISH_NOARCH_OUTPUT" =~ URL: ]] && [[ "$PUBLISH_NOARCH_OUTPUT" =~ Hash: ]]; then print_test_result "Publish tool without ARCH" 0 else print_test_result "Publish tool without ARCH" 1 @@ -402,13 +414,13 @@ EOF # Test 13c: Install universal tool (arch fallback) echo -e "\nTest 13c: Install universal tool (arch fallback)" - rm -rf ~/.config/getpkg/"${TEST_TOOL_NOARCH}.json" ~/.getpkg/"${TEST_TOOL_NOARCH}" ~/.local/bin/getpkg/"${TEST_TOOL_NOARCH}" 2>/dev/null || true + rm -rf ~/.config/getpkg/packages/"${TEST_TOOL_NOARCH}.json" ~/.getpkg/"${TEST_TOOL_NOARCH}" ~/.local/bin/getpkg/"${TEST_TOOL_NOARCH}" 2>/dev/null || true FALLBACK_INSTALL_OUTPUT=$(timeout 3 "$GETPKG" install "${TEST_TOOL_NOARCH}" 2>&1) || FALLBACK_INSTALL_OUTPUT="" # Check if tool was installed successfully and has universal architecture - if [[ "$FALLBACK_INSTALL_OUTPUT" =~ Installed\ ${TEST_TOOL_NOARCH}\ successfully ]] && [ -f ~/.config/getpkg/"${TEST_TOOL_NOARCH}.json" ]; then + if [[ "$FALLBACK_INSTALL_OUTPUT" =~ Installed\ ${TEST_TOOL_NOARCH}\ successfully ]] && [ -f ~/.config/getpkg/packages/"${TEST_TOOL_NOARCH}.json" ]; then # Verify the architecture is "universal" in the config file - INSTALLED_ARCH=$(grep -o '"arch"[[:space:]]*:[[:space:]]*"[^"]*"' ~/.config/getpkg/"${TEST_TOOL_NOARCH}.json" | sed 's/.*"\([^"]*\)".*/\1/') + INSTALLED_ARCH=$(grep -o '"arch"[[:space:]]*:[[:space:]]*"[^"]*"' ~/.config/getpkg/packages/"${TEST_TOOL_NOARCH}.json" | sed 's/.*"\([^"]*\)".*/\1/') if [ "$INSTALLED_ARCH" = "universal" ]; then print_test_result "Install universal tool with arch fallback" 0 @@ -429,10 +441,19 @@ EOF fi # Clean up the noarch tool from server - NOARCH_HASH=$(curl -s "https://getpkg.xyz/hash/${TEST_TOOL_NOARCH}" 2>/dev/null || echo "") - if [ -n "$NOARCH_HASH" ] && [ "$NOARCH_HASH" != "null" ] && [ "$NOARCH_HASH" != "Not found" ]; then - curl -s -H "Authorization: Bearer ${SOS_WRITE_TOKEN}" \ - "https://getpkg.xyz/deleteobject?hash=${NOARCH_HASH}" >/dev/null 2>&1 || true + NOARCH_HASH_RESPONSE=$(curl -s "https://getpkg.xyz/hash/${TEST_TOOL_NOARCH}" 2>/dev/null || echo "") + if [ -n "$NOARCH_HASH_RESPONSE" ]; then + # Parse JSON response to extract hash + if command -v jq >/dev/null 2>&1; then + NOARCH_HASH=$(echo "$NOARCH_HASH_RESPONSE" | jq -r '.hash' 2>/dev/null || echo "") + else + # Fallback: extract hash value using grep/sed + NOARCH_HASH=$(echo "$NOARCH_HASH_RESPONSE" | grep -o '"hash":"[^"]*"' | sed 's/.*"hash":"\([^"]*\)".*/\1/' || echo "") + fi + if [ -n "$NOARCH_HASH" ] && [ "$NOARCH_HASH" != "null" ]; then + curl -s -H "Authorization: Bearer ${SOS_WRITE_TOKEN}" \ + "https://getpkg.xyz/deleteobject?hash=${NOARCH_HASH}" >/dev/null 2>&1 || true + fi fi else echo -e "\n${YELLOW}Skipping publish/install tests (SOS_WRITE_TOKEN not set)${NC}" @@ -466,7 +487,7 @@ EOF # Publish and install the tool PUBLISH_OUTPUT=$(timeout 3 "$GETPKG" publish "${TEST_UNINSTALL_TOOL}:${TEST_ARCH}" "$UNINSTALL_DIR" 2>&1) - if [[ "$PUBLISH_OUTPUT" =~ Published! ]]; then + if [[ "$PUBLISH_OUTPUT" =~ Published.*! ]]; then INSTALL_OUTPUT=$(timeout 3 "$GETPKG" install "$TEST_UNINSTALL_TOOL" 2>&1) if [[ "$INSTALL_OUTPUT" =~ Installed\ ${TEST_UNINSTALL_TOOL}\ successfully ]]; then # Count bashrc entries before uninstall @@ -479,7 +500,7 @@ EOF SYMLINK_EXISTS=false # HELPER_SYMLINK_EXISTS=false - [ -f ~/.config/getpkg/"${TEST_UNINSTALL_TOOL}.json" ] && CONFIG_EXISTS=true + [ -f ~/.config/getpkg/packages/"${TEST_UNINSTALL_TOOL}.json" ] && CONFIG_EXISTS=true [ -d ~/.getpkg/"$TEST_UNINSTALL_TOOL" ] && TOOL_DIR_EXISTS=true [ -L ~/.local/bin/getpkg/"$TEST_UNINSTALL_TOOL" ] && SYMLINK_EXISTS=true # Check if helper symlink exists (not currently used in validation) @@ -493,7 +514,7 @@ EOF ALL_REMOVED=true # Check config file removed - if [ -f ~/.config/getpkg/"${TEST_UNINSTALL_TOOL}.json" ]; then + if [ -f ~/.config/getpkg/packages/"${TEST_UNINSTALL_TOOL}.json" ]; then echo "ERROR: Config file still exists after uninstall" ALL_REMOVED=false fi @@ -546,7 +567,7 @@ EOF fi # Always cleanup test uninstall tool from server, even if test failed - if [[ "$PUBLISH_OUTPUT" =~ Published! ]]; then + if [[ "$PUBLISH_OUTPUT" =~ Published.*! ]]; then $GETPKG unpublish "${TEST_UNINSTALL_TOOL}:${TEST_ARCH}" 2>/dev/null || true fi fi @@ -577,7 +598,7 @@ echo "Multi-arch unpublish test"' > "$UNPUBLISH_TEST_DIR/$UNPUBLISH_TOOL_MULTI" PUBLISH_aarch64_OUTPUT=$("$GETPKG" publish "${UNPUBLISH_TOOL_MULTI}:aarch64" "$UNPUBLISH_TEST_DIR" 2>&1) PUBLISH_universal_OUTPUT=$("$GETPKG" publish "${UNPUBLISH_TOOL_MULTI}:universal" "$UNPUBLISH_TEST_DIR" 2>&1) - if [[ "$PUBLISH_x86_64_OUTPUT" =~ Published! ]] && [[ "$PUBLISH_aarch64_OUTPUT" =~ Published! ]] && [[ "$PUBLISH_universal_OUTPUT" =~ Published! ]]; then + if [[ "$PUBLISH_x86_64_OUTPUT" =~ Published.*! ]] && [[ "$PUBLISH_aarch64_OUTPUT" =~ Published.*! ]] && [[ "$PUBLISH_universal_OUTPUT" =~ Published.*! ]]; then # Test robust unpublish - should remove ALL architectures sleep 1 # Give server time to process all publishes UNPUBLISH_OUTPUT=$("$GETPKG" unpublish "$UNPUBLISH_TOOL_MULTI" 2>&1) @@ -607,7 +628,7 @@ echo "Universal arch unpublish test"' > "$UNPUBLISH_TEST_DIR/$UNPUBLISH_TOOL_CUS # Publish with universal architecture PUBLISH_CUSTOM_OUTPUT=$("$GETPKG" publish "${UNPUBLISH_TOOL_CUSTOM}:universal" "$UNPUBLISH_TEST_DIR" 2>&1) - if [[ "$PUBLISH_CUSTOM_OUTPUT" =~ Published! ]]; then + if [[ "$PUBLISH_CUSTOM_OUTPUT" =~ Published.*! ]]; then # Test that unpublish can find and remove custom tags UNPUBLISH_CUSTOM_OUTPUT=$("$GETPKG" unpublish "$UNPUBLISH_TOOL_CUSTOM" 2>&1) UNPUBLISH_CUSTOM_EXIT_CODE=$? diff --git a/getpkg/test/CMakeLists.txt b/getpkg/test/CMakeLists.txt new file mode 100644 index 0000000..978f94e --- /dev/null +++ b/getpkg/test/CMakeLists.txt @@ -0,0 +1,45 @@ +# Unit Tests for getpkg multi-server support +cmake_minimum_required(VERSION 3.16) + +# Test project setup +project(getpkg_tests VERSION 1.0.0 LANGUAGES CXX) + +# Build configuration +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXE_LINKER_FLAGS "-static") +set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") +set(BUILD_SHARED_LIBS OFF) +set(CMAKE_PREFIX_PATH /usr/local) + +# Find packages +find_package(nlohmann_json REQUIRED) + +# Add module path for FindCPRStatic +list(APPEND CMAKE_MODULE_PATH "/usr/local/share/cmake/Modules") +find_package(CPRStatic REQUIRED) + +# Include directories +include_directories(../src) +include_directories(../src/common) + +# Source files from main project (excluding main.cpp) +file(GLOB_RECURSE MAIN_SOURCES "../src/*.cpp") +list(REMOVE_ITEM MAIN_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../src/main.cpp") + +# Test source files +file(GLOB_RECURSE TEST_SOURCES "*.cpp") + +# Create test executable +add_executable(getpkg_tests ${MAIN_SOURCES} ${TEST_SOURCES}) + +# Link libraries +target_link_libraries(getpkg_tests PRIVATE + nlohmann_json::nlohmann_json + cpr::cpr_static) + +# Enable testing +enable_testing() + +# Add test +add_test(NAME unit_tests COMMAND getpkg_tests) \ No newline at end of file diff --git a/getpkg/test/Dockerfile.test-build b/getpkg/test/Dockerfile.test-build new file mode 100644 index 0000000..e7168f1 --- /dev/null +++ b/getpkg/test/Dockerfile.test-build @@ -0,0 +1,14 @@ +FROM gitea.jde.nz/public/dropshell-build-base:latest + +# Copy source files +COPY . /app/ + +# Set working directory +WORKDIR /app/test + +# Build tests +RUN cmake -G Ninja -S . -B ./build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH=/usr/local +RUN cmake --build ./build + +# Run tests +CMD ["./build/getpkg_tests"] diff --git a/getpkg/test/build_and_run_tests.sh b/getpkg/test/build_and_run_tests.sh new file mode 100755 index 0000000..ce452b6 --- /dev/null +++ b/getpkg/test/build_and_run_tests.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +echo "Building and running unit tests for getpkg multi-server support..." + +# Create a temporary Dockerfile for building tests +cat > "$SCRIPT_DIR/Dockerfile.test-build" << 'EOF' +FROM gitea.jde.nz/public/dropshell-build-base:latest + +# Copy source files +COPY . /app/ + +# Set working directory +WORKDIR /app/test + +# Build tests +RUN cmake -G Ninja -S . -B ./build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH=/usr/local +RUN cmake --build ./build + +# Run tests +CMD ["./build/getpkg_tests"] +EOF + +echo "Building test container..." +docker build -t getpkg-test-build -f "$SCRIPT_DIR/Dockerfile.test-build" "$PROJECT_DIR" + +echo "Running unit tests..." +docker run --rm getpkg-test-build + +echo "Cleaning up..." +rm -f "$SCRIPT_DIR/Dockerfile.test-build" + +echo "Unit tests completed!" \ No newline at end of file diff --git a/getpkg/test/test_framework.hpp b/getpkg/test/test_framework.hpp new file mode 100644 index 0000000..427fa9a --- /dev/null +++ b/getpkg/test/test_framework.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Simple test framework +class TestRunner { +public: + using TestFunction = std::function; + + static TestRunner& instance() { + static TestRunner runner; + return runner; + } + + void addTest(const std::string& name, TestFunction test) { + tests_.push_back({name, test}); + } + + int runAllTests() { + int passed = 0; + int failed = 0; + + std::cout << "Running " << tests_.size() << " tests...\n\n"; + + for (const auto& test : tests_) { + try { + test.second(); + std::cout << "[PASS] " << test.first << std::endl; + passed++; + } catch (const std::exception& e) { + std::cout << "[FAIL] " << test.first << " - " << e.what() << std::endl; + failed++; + } catch (...) { + std::cout << "[FAIL] " << test.first << " - Unknown error" << std::endl; + failed++; + } + } + + std::cout << "\nResults: " << passed << " passed, " << failed << " failed\n"; + return failed; + } + +private: + std::vector> tests_; +}; + +// Test assertion macros +#define ASSERT_TRUE(condition) \ + if (!(condition)) { \ + throw std::runtime_error("Assertion failed: " #condition); \ + } + +#define ASSERT_FALSE(condition) \ + if (condition) { \ + throw std::runtime_error("Assertion failed: " #condition " should be false"); \ + } + +#define ASSERT_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected != actual"); \ + } + +#define ASSERT_STR_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected '" + std::string(expected) + "' but got '" + std::string(actual) + "'"); \ + } + +#define ASSERT_NOT_EMPTY(str) \ + if ((str).empty()) { \ + throw std::runtime_error("Assertion failed: string should not be empty"); \ + } + +#define ASSERT_GE(actual, expected) \ + if ((actual) < (expected)) { \ + throw std::runtime_error("Assertion failed: expected " + std::to_string(actual) + " >= " + std::to_string(expected)); \ + } + +// Test registration macro +#define TEST(suite, name) \ + void test_##suite##_##name(); \ + struct TestRegistrar_##suite##_##name { \ + TestRegistrar_##suite##_##name() { \ + TestRunner::instance().addTest(#suite "::" #name, test_##suite##_##name); \ + } \ + }; \ + static TestRegistrar_##suite##_##name registrar_##suite##_##name; \ + void test_##suite##_##name() \ No newline at end of file diff --git a/getpkg/test/test_getbin_client.cpp b/getpkg/test/test_getbin_client.cpp new file mode 100644 index 0000000..1916441 --- /dev/null +++ b/getpkg/test/test_getbin_client.cpp @@ -0,0 +1,343 @@ +#include "GetbinClient.hpp" +#include +#include +#include +#include + +// Test framework declarations from test_main.cpp +class TestRunner { +public: + static TestRunner& instance(); + void addTest(const std::string& name, std::function test); +}; + +#define ASSERT_TRUE(condition) \ + if (!(condition)) { \ + throw std::runtime_error("Assertion failed: " #condition); \ + } + +#define ASSERT_FALSE(condition) \ + if (condition) { \ + throw std::runtime_error("Assertion failed: " #condition " should be false"); \ + } + +#define ASSERT_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected != actual"); \ + } + +#define ASSERT_STR_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected '" + std::string(expected) + "' but got '" + std::string(actual) + "'"); \ + } + +#define ASSERT_NOT_EMPTY(str) \ + if ((str).empty()) { \ + throw std::runtime_error("Assertion failed: string should not be empty"); \ + } + +#define TEST(name) \ + void test_GetbinClient_##name(); \ + void register_GetbinClient_##name() { \ + TestRunner::instance().addTest("GetbinClient::" #name, test_GetbinClient_##name); \ + } \ + void test_GetbinClient_##name() + +// Test helper class for GetbinClient testing +class GetbinClientTestHelper { +public: + static std::filesystem::path createTempDir() { + auto tempDir = std::filesystem::temp_directory_path() / "getpkg_client_test" / std::to_string(std::time(nullptr)); + std::filesystem::create_directories(tempDir); + return tempDir; + } + + static void cleanupTempDir(const std::filesystem::path& dir) { + if (std::filesystem::exists(dir)) { + std::filesystem::remove_all(dir); + } + } + + static void createTestFile(const std::filesystem::path& path, const std::string& content) { + std::ofstream file(path); + file << content; + file.close(); + } + + static std::string readTestFile(const std::filesystem::path& path) { + std::ifstream file(path); + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return content; + } + + // Mock progress callback for testing + static bool mockProgressCallback(size_t downloaded, size_t total) { + return true; // Continue download + } + + static bool cancelProgressCallback(size_t downloaded, size_t total) { + return false; // Cancel download + } +}; + +TEST(DefaultConstructor) { + GetbinClient client; + + // Should work with default server + ASSERT_NOT_EMPTY(client.getLastError()); // Initially empty, but method should exist +} + +TEST(MultiServerConstructor) { + std::vector servers = {"server1.com", "server2.com", "server3.com"}; + GetbinClient client(servers); + + // Should initialize with multiple servers + ASSERT_NOT_EMPTY(client.getLastError()); // Method should exist +} + +TEST(EmptyServerList) { + std::vector emptyServers; + GetbinClient client(emptyServers); + + // Should handle empty server list gracefully + ASSERT_NOT_EMPTY(client.getLastError()); +} + +TEST(NetworkErrorClassification) { + GetbinClient client; + + // Test error message generation + ASSERT_NOT_EMPTY(client.getNetworkErrorMessage(NetworkError::ConnectionFailed)); + ASSERT_NOT_EMPTY(client.getNetworkErrorMessage(NetworkError::Timeout)); + ASSERT_NOT_EMPTY(client.getNetworkErrorMessage(NetworkError::NotFound)); + ASSERT_NOT_EMPTY(client.getNetworkErrorMessage(NetworkError::Unauthorized)); + ASSERT_NOT_EMPTY(client.getNetworkErrorMessage(NetworkError::ServerError)); +} + +TEST(UrlBuilding) { + GetbinClient client; + + // Test URL building with different server formats + // Note: This tests internal functionality, so we test through public methods + std::string hash; + + // Test with different server URL formats + bool result1 = client.getHash("http://server.com", "test-tool", "x86_64", hash); + bool result2 = client.getHash("https://server.com", "test-tool", "x86_64", hash); + bool result3 = client.getHash("server.com", "test-tool", "x86_64", hash); + + // These will fail due to network, but should not crash + ASSERT_FALSE(result1 || result2 || result3); // All should fail gracefully +} + +TEST(FindPackageServerWithMultipleServers) { + std::vector servers = {"server1.com", "server2.com", "server3.com"}; + GetbinClient client(servers); + + std::string foundServer; + bool found = client.findPackageServer("nonexistent-tool", "x86_64", foundServer); + + // Should not find nonexistent package, but should not crash + ASSERT_FALSE(found); + ASSERT_TRUE(foundServer.empty()); +} + +TEST(MultiServerDownloadFallback) { + auto tempDir = GetbinClientTestHelper::createTempDir(); + auto outputPath = tempDir / "test-download"; + + std::vector servers = {"invalid-server1.com", "invalid-server2.com", "invalid-server3.com"}; + GetbinClient client(servers); + + // Should try all servers and fail gracefully + bool result = client.download("nonexistent-tool", "x86_64", outputPath.string()); + ASSERT_FALSE(result); + + // Should have error message + ASSERT_NOT_EMPTY(client.getLastError()); + + GetbinClientTestHelper::cleanupTempDir(tempDir); +} + +TEST(ServerSpecificDownload) { + auto tempDir = GetbinClientTestHelper::createTempDir(); + auto outputPath = tempDir / "test-download"; + + GetbinClient client; + + // Test server-specific download with invalid server + NetworkError error = client.downloadFromServer("invalid-server.com", "test-tool", "x86_64", + outputPath.string()); + + // Should return appropriate network error + ASSERT_TRUE(error != NetworkError::None); + ASSERT_NOT_EMPTY(client.getNetworkErrorMessage(error)); + + GetbinClientTestHelper::cleanupTempDir(tempDir); +} + +TEST(ProgressCallbackHandling) { + auto tempDir = GetbinClientTestHelper::createTempDir(); + auto outputPath = tempDir / "test-download"; + + GetbinClient client; + + // Test with progress callback that continues + bool result1 = client.download("test-tool", "x86_64", outputPath.string(), + GetbinClientTestHelper::mockProgressCallback); + ASSERT_FALSE(result1); // Will fail due to network, but should handle callback + + // Test with progress callback that cancels + bool result2 = client.download("test-tool", "x86_64", outputPath.string(), + GetbinClientTestHelper::cancelProgressCallback); + ASSERT_FALSE(result2); // Should handle cancellation + + GetbinClientTestHelper::cleanupTempDir(tempDir); +} + +TEST(HashRetrievalMultiServer) { + std::vector servers = {"server1.com", "server2.com"}; + GetbinClient client(servers); + + std::string hash; + + // Test multi-server hash retrieval + bool result = client.getHash("test-tool", "x86_64", hash); + ASSERT_FALSE(result); // Will fail due to network + ASSERT_TRUE(hash.empty()); + + // Test server-specific hash retrieval + bool result2 = client.getHash("server1.com", "test-tool", "x86_64", hash); + ASSERT_FALSE(result2); // Will fail due to network + ASSERT_TRUE(hash.empty()); +} + +TEST(UploadFunctionality) { + auto tempDir = GetbinClientTestHelper::createTempDir(); + auto testFile = tempDir / "test-archive.tar.gz"; + + // Create a test file + GetbinClientTestHelper::createTestFile(testFile, "test archive content"); + + GetbinClient client; + std::string outUrl, outHash; + + // Test server-specific upload + bool result1 = client.upload("test-server.com", testFile.string(), outUrl, outHash, "test-token"); + ASSERT_FALSE(result1); // Will fail due to network + + // Test backward compatibility upload + bool result2 = client.upload(testFile.string(), outUrl, outHash, "test-token"); + ASSERT_FALSE(result2); // Will fail due to network + + GetbinClientTestHelper::cleanupTempDir(tempDir); +} + +TEST(LegacyMethodsCompatibility) { + GetbinClient client; + + // Test legacy delete method + bool deleteResult = client.deleteObject("test-hash", "test-token"); + ASSERT_FALSE(deleteResult); // Will fail due to network + + // Test legacy list packages method + std::vector packages; + bool listResult = client.listPackages(packages); + ASSERT_FALSE(listResult); // Will fail due to network + ASSERT_TRUE(packages.empty()); + + // Test legacy list all entries method + std::vector>> entries; + bool entriesResult = client.listAllEntries(entries); + ASSERT_FALSE(entriesResult); // Will fail due to network + ASSERT_TRUE(entries.empty()); +} + +TEST(ErrorMessagePersistence) { + GetbinClient client; + + // Trigger an error + std::string hash; + client.getHash("invalid-server.com", "test-tool", "x86_64", hash); + + // Error message should be set + std::string error1 = client.getLastError(); + ASSERT_NOT_EMPTY(error1); + + // Trigger another error + client.download("test-tool", "x86_64", "/invalid/path"); + + // Error message should be updated + std::string error2 = client.getLastError(); + ASSERT_NOT_EMPTY(error2); + + // Errors might be different depending on which fails first + // But both should be non-empty +} + +TEST(UserAgentGeneration) { + GetbinClient client; + + // Test that user agent is properly set (indirect test through network calls) + std::string hash; + bool result = client.getHash("test-server.com", "test-tool", "x86_64", hash); + + // Should fail due to network but not crash due to user agent issues + ASSERT_FALSE(result); + ASSERT_NOT_EMPTY(client.getLastError()); +} + +TEST(ConcurrentOperations) { + std::vector servers = {"server1.com", "server2.com"}; + GetbinClient client(servers); + + // Test that multiple operations can be performed without interference + std::string hash1, hash2; + + bool result1 = client.getHash("tool1", "x86_64", hash1); + bool result2 = client.getHash("tool2", "aarch64", hash2); + + // Both should fail gracefully without interfering with each other + ASSERT_FALSE(result1); + ASSERT_FALSE(result2); + ASSERT_TRUE(hash1.empty()); + ASSERT_TRUE(hash2.empty()); +} + +TEST(ArchitectureHandling) { + GetbinClient client; + + std::string hash; + + // Test different architecture strings + bool result1 = client.getHash("test-tool", "x86_64", hash); + bool result2 = client.getHash("test-tool", "aarch64", hash); + bool result3 = client.getHash("test-tool", "universal", hash); + bool result4 = client.getHash("test-tool", "invalid-arch", hash); + + // All should fail due to network but handle architectures properly + ASSERT_FALSE(result1); + ASSERT_FALSE(result2); + ASSERT_FALSE(result3); + ASSERT_FALSE(result4); +} + +// Registration function +void registerGetbinClientTests() { + register_GetbinClient_DefaultConstructor(); + register_GetbinClient_MultiServerConstructor(); + register_GetbinClient_EmptyServerList(); + register_GetbinClient_NetworkErrorClassification(); + register_GetbinClient_UrlBuilding(); + register_GetbinClient_FindPackageServerWithMultipleServers(); + register_GetbinClient_MultiServerDownloadFallback(); + register_GetbinClient_ServerSpecificDownload(); + register_GetbinClient_ProgressCallbackHandling(); + register_GetbinClient_HashRetrievalMultiServer(); + register_GetbinClient_UploadFunctionality(); + register_GetbinClient_LegacyMethodsCompatibility(); + register_GetbinClient_ErrorMessagePersistence(); + register_GetbinClient_UserAgentGeneration(); + register_GetbinClient_ConcurrentOperations(); + register_GetbinClient_ArchitectureHandling(); +} \ No newline at end of file diff --git a/getpkg/test/test_integration.cpp b/getpkg/test/test_integration.cpp new file mode 100644 index 0000000..bb70dab --- /dev/null +++ b/getpkg/test/test_integration.cpp @@ -0,0 +1,530 @@ +#include "ServerManager.hpp" +#include "GetbinClient.hpp" +#include "PackageMetadata.hpp" +#include "MigrationManager.hpp" +#include +#include +#include + +// Test framework declarations from test_main.cpp +class TestRunner { +public: + static TestRunner& instance(); + void addTest(const std::string& name, std::function test); +}; + +#define ASSERT_TRUE(condition) \ + if (!(condition)) { \ + throw std::runtime_error("Assertion failed: " #condition); \ + } + +#define ASSERT_FALSE(condition) \ + if (condition) { \ + throw std::runtime_error("Assertion failed: " #condition " should be false"); \ + } + +#define ASSERT_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected != actual"); \ + } + +#define ASSERT_STR_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected '" + std::string(expected) + "' but got '" + std::string(actual) + "'"); \ + } + +#define ASSERT_NOT_EMPTY(str) \ + if ((str).empty()) { \ + throw std::runtime_error("Assertion failed: string should not be empty"); \ + } + +#define ASSERT_GE(actual, expected) \ + if ((actual) < (expected)) { \ + throw std::runtime_error("Assertion failed: expected " + std::to_string(actual) + " >= " + std::to_string(expected)); \ + } + +#define TEST(name) \ + void test_Integration_##name(); \ + void register_Integration_##name() { \ + TestRunner::instance().addTest("Integration::" #name, test_Integration_##name); \ + } \ + void test_Integration_##name() + +// Test helper class for Integration testing +class IntegrationTestHelper { +public: + static std::filesystem::path createTempDir() { + auto tempDir = std::filesystem::temp_directory_path() / "getpkg_integration_test" / std::to_string(std::time(nullptr)); + std::filesystem::create_directories(tempDir); + return tempDir; + } + + static void cleanupTempDir(const std::filesystem::path& dir) { + if (std::filesystem::exists(dir)) { + std::filesystem::remove_all(dir); + } + } + + static void setupLegacyEnvironment(const std::filesystem::path& configDir) { + // Create legacy token file + auto legacyDir = configDir / "getpkg.xyz"; + std::filesystem::create_directories(legacyDir); + + std::ofstream tokenFile(legacyDir / "write_token.txt"); + tokenFile << "legacy-integration-token"; + tokenFile.close(); + + // Create legacy package files + nlohmann::json package1 = { + {"name", "integration-tool1"}, + {"version", "2023.1201.1000"}, + {"hash", "legacy123hash456"}, + {"arch", "x86_64"} + }; + + nlohmann::json package2 = { + {"name", "integration-tool2"}, + {"version", "2023.1202.1100"}, + {"hash", "legacy789hash012"}, + {"arch", "aarch64"} + }; + + std::ofstream package1File(configDir / "integration-tool1.json"); + package1File << package1.dump(2); + package1File.close(); + + std::ofstream package2File(configDir / "integration-tool2.json"); + package2File << package2.dump(2); + package2File.close(); + } + + static void verifyNewFormatStructure(const std::filesystem::path& configDir) { + ASSERT_TRUE(std::filesystem::exists(configDir / "servers.json")); + ASSERT_TRUE(std::filesystem::exists(configDir / "packages")); + ASSERT_TRUE(std::filesystem::is_directory(configDir / "packages")); + } + + static void setEnvironmentHome(const std::filesystem::path& homeDir) { + setenv("HOME", homeDir.c_str(), 1); + } +}; + +TEST(CompleteWorkflowFromLegacyToMultiServer) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + + // Set up legacy environment + IntegrationTestHelper::setupLegacyEnvironment(configDir); + IntegrationTestHelper::setEnvironmentHome(tempDir); + + // Step 1: Migration Manager detects need for migration + MigrationManager migrationManager(configDir); + ASSERT_TRUE(migrationManager.needsMigration()); + + // Step 2: Perform migration + bool migrationResult = migrationManager.performMigration(); + ASSERT_TRUE(migrationResult); + + // Verify migration results + auto result = migrationManager.getLastMigrationResult(); + ASSERT_TRUE(result.success); + ASSERT_EQ(2, result.migratedPackages); + ASSERT_TRUE(result.serverConfigMigrated); + + // Step 3: ServerManager loads new configuration + ServerManager serverManager; + auto loadResult = serverManager.loadConfiguration(); + ASSERT_EQ(ServerManagerError::None, loadResult); + + auto servers = serverManager.getServers(); + ASSERT_EQ(1, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + ASSERT_TRUE(serverManager.hasWriteToken("getpkg.xyz")); + + // Step 4: Add additional servers + auto addResult = serverManager.addServer("packages.example.com"); + ASSERT_EQ(ServerManagerError::None, addResult); + + serverManager.setWriteToken("packages.example.com", "example-token"); + + // Step 5: GetbinClient uses multi-server configuration + auto updatedServers = serverManager.getServers(); + GetbinClient client(updatedServers); + + // Test multi-server operations (will fail due to network but should not crash) + std::string hash; + bool hashResult = client.getHash("test-tool", "x86_64", hash); + ASSERT_FALSE(hashResult); // Expected to fail due to network + ASSERT_NOT_EMPTY(client.getLastError()); + + // Step 6: PackageMetadataManager works with migrated data + PackageMetadataManager packageManager(configDir); + + auto installedPackages = packageManager.listInstalledPackages(); + ASSERT_EQ(2, installedPackages.size()); + + // Verify migrated packages have server information + PackageMetadata tool1 = packageManager.loadPackageMetadata("integration-tool1"); + ASSERT_TRUE(tool1.isValid()); + ASSERT_STR_EQ("getpkg.xyz", tool1.sourceServer); + ASSERT_NOT_EMPTY(tool1.installDate); + + // Step 7: Save new package with multi-server metadata + PackageMetadata newPackage("new-tool", "2024.0115.1430", "new123hash456", "x86_64", "packages.example.com"); + bool saveResult = packageManager.savePackageMetadata(newPackage); + ASSERT_TRUE(saveResult); + + // Verify new package is tracked + auto updatedPackages = packageManager.listInstalledPackages(); + ASSERT_EQ(3, updatedPackages.size()); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(ServerManagerAndGetbinClientIntegration) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + IntegrationTestHelper::setEnvironmentHome(tempDir); + + // Initialize ServerManager + ServerManager serverManager; + serverManager.ensureDefaultConfiguration(); + + // Add multiple servers + serverManager.addServer("server1.example.com"); + serverManager.addServer("server2.example.com"); + serverManager.setWriteToken("server1.example.com", "token1"); + serverManager.setWriteToken("server2.example.com", "token2"); + + // Get server list for client + auto servers = serverManager.getServers(); + ASSERT_EQ(3, servers.size()); // default + 2 added + + // Initialize GetbinClient with server list + GetbinClient client(servers); + + // Test server-specific operations + std::string foundServer; + bool findResult = client.findPackageServer("test-tool", "x86_64", foundServer); + ASSERT_FALSE(findResult); // Will fail due to network + + // Test fallback behavior + std::string hash; + bool hashResult = client.getHash("test-tool", "x86_64", hash); + ASSERT_FALSE(hashResult); // Will fail but should try all servers + + // Verify error handling + ASSERT_NOT_EMPTY(client.getLastError()); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(PackageMetadataAndMigrationIntegration) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + + // Create legacy package files + nlohmann::json legacyPackage1 = { + {"name", "metadata-tool1"}, + {"version", "1.0"}, + {"hash", "hash1"}, + {"arch", "x86_64"} + }; + + nlohmann::json legacyPackage2 = { + {"name", "metadata-tool2"}, + {"version", "2.0"}, + {"hash", "hash2"}, + {"arch", "aarch64"} + }; + + std::ofstream file1(configDir / "metadata-tool1.json"); + file1 << legacyPackage1.dump(2); + file1.close(); + + std::ofstream file2(configDir / "metadata-tool2.json"); + file2 << legacyPackage2.dump(2); + file2.close(); + + // Use MigrationManager to migrate + MigrationManager migrationManager(configDir); + ASSERT_TRUE(migrationManager.needsMigration()); + + bool migrationResult = migrationManager.performMigration(); + ASSERT_TRUE(migrationResult); + + // Use PackageMetadataManager to verify migration + PackageMetadataManager packageManager(configDir); + + auto packages = packageManager.listInstalledPackages(); + ASSERT_EQ(2, packages.size()); + + // Verify migrated metadata + PackageMetadata tool1 = packageManager.loadPackageMetadata("metadata-tool1"); + ASSERT_TRUE(tool1.isValid()); + ASSERT_STR_EQ("metadata-tool1", tool1.name); + ASSERT_STR_EQ("1.0", tool1.version); + ASSERT_STR_EQ("getpkg.xyz", tool1.sourceServer); // Should be set during migration + ASSERT_NOT_EMPTY(tool1.installDate); + + PackageMetadata tool2 = packageManager.loadPackageMetadata("metadata-tool2"); + ASSERT_TRUE(tool2.isValid()); + ASSERT_STR_EQ("aarch64", tool2.arch); + ASSERT_STR_EQ("getpkg.xyz", tool2.sourceServer); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(EndToEndPackageInstallationWorkflow) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + IntegrationTestHelper::setEnvironmentHome(tempDir); + + // Step 1: Initialize ServerManager with multiple servers + ServerManager serverManager; + serverManager.ensureDefaultConfiguration(); + serverManager.addServer("primary.packages.com"); + serverManager.addServer("backup.packages.com"); + + // Step 2: Initialize GetbinClient with server list + auto servers = serverManager.getServers(); + GetbinClient client(servers); + + // Step 3: Initialize PackageMetadataManager + PackageMetadataManager packageManager(configDir); + packageManager.ensurePackagesDirectory(); + + // Step 4: Simulate package installation workflow + std::string toolName = "workflow-test-tool"; + std::string version = "2024.0115.1430"; + std::string hash = "workflow123hash456"; + std::string arch = "x86_64"; + std::string sourceServer = "primary.packages.com"; + + // Try to download (will fail due to network but tests the workflow) + auto downloadPath = tempDir / "downloads" / (toolName + ".tar.gz"); + std::filesystem::create_directories(downloadPath.parent_path()); + + bool downloadResult = client.download(toolName, arch, downloadPath.string()); + ASSERT_FALSE(downloadResult); // Expected to fail due to network + + // Simulate successful installation by creating metadata + PackageMetadata metadata(toolName, version, hash, arch, sourceServer); + bool saveResult = packageManager.savePackageMetadata(metadata); + ASSERT_TRUE(saveResult); + + // Step 5: Verify package is tracked + ASSERT_TRUE(packageManager.packageExists(toolName)); + + PackageMetadata savedMetadata = packageManager.loadPackageMetadata(toolName); + ASSERT_TRUE(savedMetadata.isValid()); + ASSERT_STR_EQ(sourceServer, savedMetadata.sourceServer); + + // Step 6: Simulate update check + bool needsUpdate = savedMetadata.needsUpdate("different-hash"); + ASSERT_TRUE(needsUpdate); + + bool noUpdateNeeded = savedMetadata.needsUpdate(hash); + ASSERT_FALSE(noUpdateNeeded); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(MultiServerPublishingWorkflow) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + IntegrationTestHelper::setEnvironmentHome(tempDir); + + // Initialize ServerManager with publishing tokens + ServerManager serverManager; + serverManager.ensureDefaultConfiguration(); + serverManager.addServer("publish1.example.com"); + serverManager.addServer("publish2.example.com"); + + serverManager.setWriteToken("getpkg.xyz", "default-token"); + serverManager.setWriteToken("publish1.example.com", "publish1-token"); + serverManager.setWriteToken("publish2.example.com", "publish2-token"); + + // Test default publish server selection + std::string defaultPublishServer = serverManager.getDefaultPublishServer(); + ASSERT_STR_EQ("getpkg.xyz", defaultPublishServer); // First server with token + + // Test servers with tokens + auto serversWithTokens = serverManager.getServersWithTokens(); + ASSERT_EQ(3, serversWithTokens.size()); + + // Initialize GetbinClient for publishing + auto servers = serverManager.getServers(); + GetbinClient client(servers); + + // Create test archive for publishing + auto testArchive = tempDir / "test-package.tar.gz"; + std::ofstream archiveFile(testArchive); + archiveFile << "test archive content"; + archiveFile.close(); + + // Test server-specific publishing (will fail due to network) + std::string outUrl, outHash; + bool publishResult = client.upload("publish1.example.com", testArchive.string(), + outUrl, outHash, "publish1-token"); + ASSERT_FALSE(publishResult); // Expected to fail due to network + + // Test default publishing + bool defaultPublishResult = client.upload(testArchive.string(), outUrl, outHash, "default-token"); + ASSERT_FALSE(defaultPublishResult); // Expected to fail due to network + + // Verify error handling + ASSERT_NOT_EMPTY(client.getLastError()); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(ErrorHandlingAndRecoveryWorkflow) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + + // Create corrupted configuration + std::ofstream corruptedConfig(configDir / "servers.json"); + corruptedConfig << "{ invalid json"; + corruptedConfig.close(); + + // ServerManager should recover from corruption + ServerManager serverManager; + auto loadResult = serverManager.loadConfiguration(); + ASSERT_EQ(ServerManagerError::None, loadResult); // Should recover + + auto servers = serverManager.getServers(); + ASSERT_EQ(1, servers.size()); // Should have default server + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + + // Create corrupted package metadata + std::filesystem::create_directories(configDir / "packages"); + std::ofstream corruptedPackage(configDir / "packages" / "corrupted-tool.json"); + corruptedPackage << "{ invalid json"; + corruptedPackage.close(); + + // PackageMetadataManager should handle corruption + PackageMetadataManager packageManager(configDir); + + PackageMetadata corruptedMetadata = packageManager.loadPackageMetadata("corrupted-tool"); + ASSERT_FALSE(corruptedMetadata.isValid()); // Should fail gracefully + + // Cleanup invalid metadata + int cleanedCount = packageManager.cleanupInvalidMetadata(); + ASSERT_EQ(1, cleanedCount); + + // Verify cleanup worked + ASSERT_FALSE(packageManager.packageExists("corrupted-tool")); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(BackwardCompatibilityWorkflow) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + IntegrationTestHelper::setEnvironmentHome(tempDir); + + // Test that system works without any configuration (backward compatibility) + ServerManager serverManager; + serverManager.ensureDefaultConfiguration(); + + auto servers = serverManager.getServers(); + ASSERT_EQ(1, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + + // GetbinClient should work with default configuration + GetbinClient defaultClient; // Default constructor + + std::string hash; + bool hashResult = defaultClient.getHash("test-tool", "x86_64", hash); + ASSERT_FALSE(hashResult); // Will fail due to network + + // Multi-server client should also work + GetbinClient multiClient(servers); + bool multiHashResult = multiClient.getHash("test-tool", "x86_64", hash); + ASSERT_FALSE(multiHashResult); // Will fail due to network + + // PackageMetadataManager should work with default structure + PackageMetadataManager packageManager(configDir); + bool dirResult = packageManager.ensurePackagesDirectory(); + ASSERT_TRUE(dirResult); + + // Should be able to save and load packages + PackageMetadata testPackage("compat-tool", "1.0", "hash", "x86_64", "getpkg.xyz"); + bool saveResult = packageManager.savePackageMetadata(testPackage); + ASSERT_TRUE(saveResult); + + PackageMetadata loadedPackage = packageManager.loadPackageMetadata("compat-tool"); + ASSERT_TRUE(loadedPackage.isValid()); + ASSERT_STR_EQ("compat-tool", loadedPackage.name); + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +TEST(ConcurrentOperationsWorkflow) { + auto tempDir = IntegrationTestHelper::createTempDir(); + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + IntegrationTestHelper::setEnvironmentHome(tempDir); + + // Initialize components + ServerManager serverManager; + serverManager.ensureDefaultConfiguration(); + serverManager.addServer("concurrent1.example.com"); + serverManager.addServer("concurrent2.example.com"); + + auto servers = serverManager.getServers(); + GetbinClient client(servers); + + PackageMetadataManager packageManager(configDir); + packageManager.ensurePackagesDirectory(); + + // Simulate concurrent operations + std::vector tools = {"tool1", "tool2", "tool3", "tool4", "tool5"}; + + // Save multiple packages concurrently (simulated) + for (const auto& tool : tools) { + PackageMetadata metadata(tool, "1.0", "hash-" + tool, "x86_64", "concurrent1.example.com"); + bool saveResult = packageManager.savePackageMetadata(metadata); + ASSERT_TRUE(saveResult); + } + + // Verify all packages were saved + auto installedPackages = packageManager.listInstalledPackages(); + ASSERT_EQ(5, installedPackages.size()); + + // Test concurrent hash requests (will fail due to network but test concurrency) + for (const auto& tool : tools) { + std::string hash; + bool hashResult = client.getHash(tool, "x86_64", hash); + ASSERT_FALSE(hashResult); // Expected to fail + } + + // Verify no interference between operations + for (const auto& tool : tools) { + PackageMetadata metadata = packageManager.loadPackageMetadata(tool); + ASSERT_TRUE(metadata.isValid()); + ASSERT_STR_EQ(tool, metadata.name); + } + + IntegrationTestHelper::cleanupTempDir(tempDir); +} + +// Registration function +void registerIntegrationTests() { + register_Integration_CompleteWorkflowFromLegacyToMultiServer(); + register_Integration_ServerManagerAndGetbinClientIntegration(); + register_Integration_PackageMetadataAndMigrationIntegration(); + register_Integration_EndToEndPackageInstallationWorkflow(); + register_Integration_MultiServerPublishingWorkflow(); + register_Integration_ErrorHandlingAndRecoveryWorkflow(); + register_Integration_BackwardCompatibilityWorkflow(); + register_Integration_ConcurrentOperationsWorkflow(); +} \ No newline at end of file diff --git a/getpkg/test/test_main.cpp b/getpkg/test/test_main.cpp new file mode 100644 index 0000000..f3e00d4 --- /dev/null +++ b/getpkg/test/test_main.cpp @@ -0,0 +1,18 @@ +#include "test_framework.hpp" + +int main() { + std::cout << "=== getpkg Multi-Server Support Unit Tests ===" << std::endl; + std::cout << "Testing all components for multi-server functionality" << std::endl; + std::cout << std::endl; + + // Run all tests (automatically registered via static constructors) + int result = TestRunner::instance().runAllTests(); + + if (result == 0) { + std::cout << std::endl << "🎉 All tests passed! Multi-server support is working correctly." << std::endl; + } else { + std::cout << std::endl << "❌ Some tests failed. Please review the failures above." << std::endl; + } + + return result; +} \ No newline at end of file diff --git a/getpkg/test/test_migration_manager.cpp b/getpkg/test/test_migration_manager.cpp new file mode 100644 index 0000000..a69894a --- /dev/null +++ b/getpkg/test/test_migration_manager.cpp @@ -0,0 +1,545 @@ +#include "MigrationManager.hpp" +#include +#include +#include + +// Test framework declarations from test_main.cpp +class TestRunner { +public: + static TestRunner& instance(); + void addTest(const std::string& name, std::function test); +}; + +#define ASSERT_TRUE(condition) \ + if (!(condition)) { \ + throw std::runtime_error("Assertion failed: " #condition); \ + } + +#define ASSERT_FALSE(condition) \ + if (condition) { \ + throw std::runtime_error("Assertion failed: " #condition " should be false"); \ + } + +#define ASSERT_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected != actual"); \ + } + +#define ASSERT_STR_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected '" + std::string(expected) + "' but got '" + std::string(actual) + "'"); \ + } + +#define ASSERT_NOT_EMPTY(str) \ + if ((str).empty()) { \ + throw std::runtime_error("Assertion failed: string should not be empty"); \ + } + +#define ASSERT_GE(actual, expected) \ + if ((actual) < (expected)) { \ + throw std::runtime_error("Assertion failed: expected " + std::to_string(actual) + " >= " + std::to_string(expected)); \ + } + +#define TEST(name) \ + void test_MigrationManager_##name(); \ + void register_MigrationManager_##name() { \ + TestRunner::instance().addTest("MigrationManager::" #name, test_MigrationManager_##name); \ + } \ + void test_MigrationManager_##name() + +// Test helper class for MigrationManager testing +class MigrationManagerTestHelper { +public: + static std::filesystem::path createTempDir() { + auto tempDir = std::filesystem::temp_directory_path() / "getpkg_migration_test" / std::to_string(std::time(nullptr)); + std::filesystem::create_directories(tempDir); + return tempDir; + } + + static void cleanupTempDir(const std::filesystem::path& dir) { + if (std::filesystem::exists(dir)) { + std::filesystem::remove_all(dir); + } + } + + static void createLegacyTokenFile(const std::filesystem::path& configDir, const std::string& token) { + auto legacyDir = configDir / "getpkg.xyz"; + std::filesystem::create_directories(legacyDir); + + std::ofstream tokenFile(legacyDir / "write_token.txt"); + tokenFile << token; + tokenFile.close(); + } + + static void createLegacyPackageFile(const std::filesystem::path& configDir, const std::string& toolName, const nlohmann::json& content) { + std::ofstream packageFile(configDir / (toolName + ".json")); + packageFile << content.dump(2); + packageFile.close(); + } + + static void createNewFormatConfig(const std::filesystem::path& configDir) { + nlohmann::json config = { + {"version", "1.0"}, + {"servers", { + { + {"url", "getpkg.xyz"}, + {"name", "Official getpkg Registry"}, + {"default", true}, + {"writeToken", ""}, + {"added", "2024-01-15T10:30:00Z"} + } + }}, + {"lastUpdated", "2024-01-15T10:30:00Z"} + }; + + std::ofstream configFile(configDir / "servers.json"); + configFile << config.dump(2); + configFile.close(); + } + + static nlohmann::json createLegacyPackageJson(const std::string& name, const std::string& version, const std::string& hash, const std::string& arch) { + return nlohmann::json{ + {"name", name}, + {"version", version}, + {"hash", hash}, + {"arch", arch} + }; + } + + static void createPackagesDirectory(const std::filesystem::path& configDir) { + std::filesystem::create_directories(configDir / "packages"); + } + + static bool fileExists(const std::filesystem::path& path) { + return std::filesystem::exists(path); + } + + static int countFilesInDirectory(const std::filesystem::path& dir, const std::string& extension = "") { + if (!std::filesystem::exists(dir)) return 0; + + int count = 0; + for (const auto& entry : std::filesystem::directory_iterator(dir)) { + if (entry.is_regular_file()) { + if (extension.empty() || entry.path().extension() == extension) { + count++; + } + } + } + return count; + } +}; + +TEST(DefaultConstructor) { + MigrationManager manager; + + // Should initialize without crashing + // Actual functionality depends on environment setup +} + +TEST(CustomConstructor) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + MigrationManager manager(tempDir); + + // Should initialize with custom config directory + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(NeedsMigrationNoLegacyData) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + MigrationManager manager(tempDir); + + // No legacy data - should not need migration + ASSERT_FALSE(manager.needsMigration()); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(NeedsMigrationWithLegacyToken) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create legacy token file + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "legacy-token-123"); + + MigrationManager manager(tempDir); + + // Should need migration due to legacy token + ASSERT_TRUE(manager.needsMigration()); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(NeedsMigrationWithLegacyPackages) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create legacy package files + auto legacyPackage = MigrationManagerTestHelper::createLegacyPackageJson("test-tool", "1.0", "hash123", "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "test-tool", legacyPackage); + + MigrationManager manager(tempDir); + + // Should need migration due to legacy packages + ASSERT_TRUE(manager.needsMigration()); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(NeedsMigrationWithNewFormat) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create new format configuration + MigrationManagerTestHelper::createNewFormatConfig(tempDir); + MigrationManagerTestHelper::createPackagesDirectory(tempDir); + + MigrationManager manager(tempDir); + + // Should not need migration - already in new format + ASSERT_FALSE(manager.needsMigration()); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(MigrateServerConfigurationSuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create legacy token file + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "legacy-token-456"); + + MigrationManager manager(tempDir); + + // Migrate server configuration + bool result = manager.migrateServerConfiguration(); + ASSERT_TRUE(result); + + // Verify servers.json was created + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "servers.json")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(MigratePackageMetadataSuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create legacy package files + auto package1 = MigrationManagerTestHelper::createLegacyPackageJson("tool1", "1.0", "hash1", "x86_64"); + auto package2 = MigrationManagerTestHelper::createLegacyPackageJson("tool2", "2.0", "hash2", "aarch64"); + + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "tool1", package1); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "tool2", package2); + + MigrationManager manager(tempDir); + + // Migrate package metadata + bool result = manager.migratePackageMetadata(); + ASSERT_TRUE(result); + + // Verify packages directory was created + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages")); + + // Verify package files were moved and updated + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages" / "tool1.json")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages" / "tool2.json")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(CreatePackagesDirectorySuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + MigrationManager manager(tempDir); + + // Create packages directory + bool result = manager.createPackagesDirectory(); + ASSERT_TRUE(result); + + // Verify directory was created + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages")); + ASSERT_TRUE(std::filesystem::is_directory(tempDir / "packages")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(PerformFullMigrationSuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Set up legacy environment + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "full-migration-token"); + + auto package1 = MigrationManagerTestHelper::createLegacyPackageJson("migrate-tool1", "1.0", "hash1", "x86_64"); + auto package2 = MigrationManagerTestHelper::createLegacyPackageJson("migrate-tool2", "2.0", "hash2", "aarch64"); + + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "migrate-tool1", package1); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "migrate-tool2", package2); + + MigrationManager manager(tempDir); + + // Verify migration is needed + ASSERT_TRUE(manager.needsMigration()); + + // Perform full migration + bool result = manager.performMigration(); + ASSERT_TRUE(result); + + // Verify migration results + auto migrationResult = manager.getLastMigrationResult(); + ASSERT_TRUE(migrationResult.success); + ASSERT_EQ(2, migrationResult.migratedPackages); + ASSERT_EQ(2, migrationResult.totalPackages); + ASSERT_TRUE(migrationResult.serverConfigMigrated); + ASSERT_TRUE(migrationResult.packageDirectoryCreated); + + // Verify new format files exist + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "servers.json")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages" / "migrate-tool1.json")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages" / "migrate-tool2.json")); + + // Verify no longer needs migration + ASSERT_FALSE(manager.needsMigration()); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(PerformMigrationNoLegacyData) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + MigrationManager manager(tempDir); + + // Should not need migration + ASSERT_FALSE(manager.needsMigration()); + + // Migration should succeed but do nothing + bool result = manager.performMigration(); + ASSERT_TRUE(result); + + auto migrationResult = manager.getLastMigrationResult(); + ASSERT_TRUE(migrationResult.success); + ASSERT_EQ(0, migrationResult.migratedPackages); + ASSERT_EQ(0, migrationResult.totalPackages); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ValidateMigrationSuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create proper new format structure + MigrationManagerTestHelper::createNewFormatConfig(tempDir); + MigrationManagerTestHelper::createPackagesDirectory(tempDir); + + MigrationManager manager(tempDir); + + // Validation should pass + bool result = manager.validateMigration(); + ASSERT_TRUE(result); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ValidateMigrationFailure) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create incomplete structure (missing packages directory) + MigrationManagerTestHelper::createNewFormatConfig(tempDir); + // Don't create packages directory + + MigrationManager manager(tempDir); + + // Validation should fail + bool result = manager.validateMigration(); + ASSERT_FALSE(result); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(CreateBackupSuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create some files to backup + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "backup-token"); + auto package = MigrationManagerTestHelper::createLegacyPackageJson("backup-tool", "1.0", "hash", "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "backup-tool", package); + + MigrationManager manager(tempDir); + + // Create backup + bool result = manager.createBackup(); + ASSERT_TRUE(result); + + // Verify backup directory exists + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "migration_backup")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(RollbackCapability) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Set up legacy environment + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "rollback-token"); + auto package = MigrationManagerTestHelper::createLegacyPackageJson("rollback-tool", "1.0", "hash", "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "rollback-tool", package); + + MigrationManager manager(tempDir); + + // Initially should not be able to rollback + ASSERT_FALSE(manager.canRollback()); + + // Create backup + manager.createBackup(); + + // Now should be able to rollback + ASSERT_TRUE(manager.canRollback()); + + // Perform migration + manager.performMigration(); + + // Should still be able to rollback + ASSERT_TRUE(manager.canRollback()); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(PerformRollbackSuccess) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Set up legacy environment + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "rollback-test-token"); + auto package = MigrationManagerTestHelper::createLegacyPackageJson("rollback-test-tool", "1.0", "hash", "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "rollback-test-tool", package); + + MigrationManager manager(tempDir); + + // Create backup and perform migration + manager.createBackup(); + manager.performMigration(); + + // Verify migration completed + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "servers.json")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages")); + + // Perform rollback + bool rollbackResult = manager.performRollback(); + ASSERT_TRUE(rollbackResult); + + // Verify rollback restored original state + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "getpkg.xyz" / "write_token.txt")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "rollback-test-tool.json")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(MigrationWithCorruptedLegacyData) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create corrupted legacy package file + std::ofstream corruptedFile(tempDir / "corrupted-tool.json"); + corruptedFile << "{ invalid json content"; + corruptedFile.close(); + + // Create valid legacy data too + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "valid-token"); + auto validPackage = MigrationManagerTestHelper::createLegacyPackageJson("valid-tool", "1.0", "hash", "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "valid-tool", validPackage); + + MigrationManager manager(tempDir); + + // Should still need migration + ASSERT_TRUE(manager.needsMigration()); + + // Migration should handle corrupted data gracefully + bool result = manager.performMigration(); + ASSERT_TRUE(result); // Should succeed despite corrupted data + + auto migrationResult = manager.getLastMigrationResult(); + ASSERT_TRUE(migrationResult.success); + ASSERT_GE(migrationResult.errors.size(), 0); // May have errors for corrupted data + ASSERT_GE(migrationResult.warnings.size(), 0); // May have warnings + + // Valid data should be migrated + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages" / "valid-tool.json")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(MigrationResultReporting) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Set up test environment with multiple packages + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "result-test-token"); + + for (int i = 1; i <= 3; i++) { + auto package = MigrationManagerTestHelper::createLegacyPackageJson( + "result-tool" + std::to_string(i), "1.0", "hash" + std::to_string(i), "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "result-tool" + std::to_string(i), package); + } + + MigrationManager manager(tempDir); + + // Perform migration + bool result = manager.performMigration(); + ASSERT_TRUE(result); + + // Check detailed results + auto migrationResult = manager.getLastMigrationResult(); + ASSERT_TRUE(migrationResult.success); + ASSERT_EQ(3, migrationResult.migratedPackages); + ASSERT_EQ(3, migrationResult.totalPackages); + ASSERT_TRUE(migrationResult.serverConfigMigrated); + ASSERT_TRUE(migrationResult.packageDirectoryCreated); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(MigrationWithExistingNewFormatData) { + auto tempDir = MigrationManagerTestHelper::createTempDir(); + + // Create both legacy and new format data + MigrationManagerTestHelper::createLegacyTokenFile(tempDir, "mixed-token"); + auto legacyPackage = MigrationManagerTestHelper::createLegacyPackageJson("legacy-mixed", "1.0", "hash", "x86_64"); + MigrationManagerTestHelper::createLegacyPackageFile(tempDir, "legacy-mixed", legacyPackage); + + // Also create new format data + MigrationManagerTestHelper::createNewFormatConfig(tempDir); + MigrationManagerTestHelper::createPackagesDirectory(tempDir); + + MigrationManager manager(tempDir); + + // Should still need migration due to legacy data + ASSERT_TRUE(manager.needsMigration()); + + // Migration should handle mixed environment + bool result = manager.performMigration(); + ASSERT_TRUE(result); + + // Should preserve existing new format data and add legacy data + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "servers.json")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages")); + ASSERT_TRUE(MigrationManagerTestHelper::fileExists(tempDir / "packages" / "legacy-mixed.json")); + + MigrationManagerTestHelper::cleanupTempDir(tempDir); +} + +// Registration function +void registerMigrationManagerTests() { + register_MigrationManager_DefaultConstructor(); + register_MigrationManager_CustomConstructor(); + register_MigrationManager_NeedsMigrationNoLegacyData(); + register_MigrationManager_NeedsMigrationWithLegacyToken(); + register_MigrationManager_NeedsMigrationWithLegacyPackages(); + register_MigrationManager_NeedsMigrationWithNewFormat(); + register_MigrationManager_MigrateServerConfigurationSuccess(); + register_MigrationManager_MigratePackageMetadataSuccess(); + register_MigrationManager_CreatePackagesDirectorySuccess(); + register_MigrationManager_PerformFullMigrationSuccess(); + register_MigrationManager_PerformMigrationNoLegacyData(); + register_MigrationManager_ValidateMigrationSuccess(); + register_MigrationManager_ValidateMigrationFailure(); + register_MigrationManager_CreateBackupSuccess(); + register_MigrationManager_RollbackCapability(); + register_MigrationManager_PerformRollbackSuccess(); + register_MigrationManager_MigrationWithCorruptedLegacyData(); + register_MigrationManager_MigrationResultReporting(); + register_MigrationManager_MigrationWithExistingNewFormatData(); +} \ No newline at end of file diff --git a/getpkg/test/test_package_metadata.cpp b/getpkg/test/test_package_metadata.cpp new file mode 100644 index 0000000..d4708a7 --- /dev/null +++ b/getpkg/test/test_package_metadata.cpp @@ -0,0 +1,545 @@ +#include "PackageMetadata.hpp" +#include +#include +#include + +// Test framework declarations from test_main.cpp +class TestRunner { +public: + static TestRunner& instance(); + void addTest(const std::string& name, std::function test); +}; + +#define ASSERT_TRUE(condition) \ + if (!(condition)) { \ + throw std::runtime_error("Assertion failed: " #condition); \ + } + +#define ASSERT_FALSE(condition) \ + if (condition) { \ + throw std::runtime_error("Assertion failed: " #condition " should be false"); \ + } + +#define ASSERT_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected != actual"); \ + } + +#define ASSERT_STR_EQ(expected, actual) \ + if ((expected) != (actual)) { \ + throw std::runtime_error("Assertion failed: expected '" + std::string(expected) + "' but got '" + std::string(actual) + "'"); \ + } + +#define ASSERT_NOT_EMPTY(str) \ + if ((str).empty()) { \ + throw std::runtime_error("Assertion failed: string should not be empty"); \ + } + +#define TEST(name) \ + void test_PackageMetadata_##name(); \ + void register_PackageMetadata_##name() { \ + TestRunner::instance().addTest("PackageMetadata::" #name, test_PackageMetadata_##name); \ + } \ + void test_PackageMetadata_##name() + +// Test helper class for PackageMetadata testing +class PackageMetadataTestHelper { +public: + static std::filesystem::path createTempDir() { + auto tempDir = std::filesystem::temp_directory_path() / "getpkg_metadata_test" / std::to_string(std::time(nullptr)); + std::filesystem::create_directories(tempDir); + return tempDir; + } + + static void cleanupTempDir(const std::filesystem::path& dir) { + if (std::filesystem::exists(dir)) { + std::filesystem::remove_all(dir); + } + } + + static void createJsonFile(const std::filesystem::path& path, const nlohmann::json& content) { + std::ofstream file(path); + file << content.dump(2); + file.close(); + } + + static nlohmann::json readJsonFile(const std::filesystem::path& path) { + std::ifstream file(path); + nlohmann::json j; + file >> j; + return j; + } + + static PackageMetadata createValidMetadata() { + return PackageMetadata("test-tool", "2024.0115.1430", "abc123hash456", "x86_64", "getpkg.xyz", "2024-01-15T14:30:00Z"); + } + + static nlohmann::json createLegacyJson() { + return nlohmann::json{ + {"name", "legacy-tool"}, + {"version", "2023.1201.1000"}, + {"hash", "legacy123hash456"}, + {"arch", "x86_64"} + // Note: no sourceServer or installDate fields + }; + } + + static nlohmann::json createCorruptedJson() { + return nlohmann::json{ + {"name", ""}, // Invalid empty name + {"version", "invalid-version-format"}, + {"hash", "short"}, // Too short hash + {"arch", "invalid-arch"}, + {"sourceServer", "not-a-url"}, + {"installDate", "invalid-date-format"} + }; + } +}; + +TEST(DefaultConstructor) { + PackageMetadata metadata; + + ASSERT_TRUE(metadata.name.empty()); + ASSERT_TRUE(metadata.version.empty()); + ASSERT_TRUE(metadata.hash.empty()); + ASSERT_TRUE(metadata.arch.empty()); + ASSERT_TRUE(metadata.sourceServer.empty()); + ASSERT_TRUE(metadata.installDate.empty()); + + ASSERT_FALSE(metadata.isValid()); +} + +TEST(ParameterizedConstructor) { + PackageMetadata metadata("test-tool", "2024.0115.1430", "abc123hash456", "x86_64", "getpkg.xyz", "2024-01-15T14:30:00Z"); + + ASSERT_STR_EQ("test-tool", metadata.name); + ASSERT_STR_EQ("2024.0115.1430", metadata.version); + ASSERT_STR_EQ("abc123hash456", metadata.hash); + ASSERT_STR_EQ("x86_64", metadata.arch); + ASSERT_STR_EQ("getpkg.xyz", metadata.sourceServer); + ASSERT_STR_EQ("2024-01-15T14:30:00Z", metadata.installDate); + + ASSERT_TRUE(metadata.isValid()); +} + +TEST(JsonSerialization) { + PackageMetadata original = PackageMetadataTestHelper::createValidMetadata(); + + nlohmann::json j = original.toJson(); + + ASSERT_STR_EQ("test-tool", j["name"]); + ASSERT_STR_EQ("2024.0115.1430", j["version"]); + ASSERT_STR_EQ("abc123hash456", j["hash"]); + ASSERT_STR_EQ("x86_64", j["arch"]); + ASSERT_STR_EQ("getpkg.xyz", j["sourceServer"]); + ASSERT_STR_EQ("2024-01-15T14:30:00Z", j["installDate"]); +} + +TEST(JsonDeserialization) { + nlohmann::json j = { + {"name", "deserialized-tool"}, + {"version", "2024.0116.0900"}, + {"hash", "def456hash789"}, + {"arch", "aarch64"}, + {"sourceServer", "packages.example.com"}, + {"installDate", "2024-01-16T09:00:00Z"} + }; + + PackageMetadata metadata = PackageMetadata::fromJson(j); + + ASSERT_STR_EQ("deserialized-tool", metadata.name); + ASSERT_STR_EQ("2024.0116.0900", metadata.version); + ASSERT_STR_EQ("def456hash789", metadata.hash); + ASSERT_STR_EQ("aarch64", metadata.arch); + ASSERT_STR_EQ("packages.example.com", metadata.sourceServer); + ASSERT_STR_EQ("2024-01-16T09:00:00Z", metadata.installDate); + + ASSERT_TRUE(metadata.isValid()); +} + +TEST(LegacyJsonMigration) { + nlohmann::json legacyJson = PackageMetadataTestHelper::createLegacyJson(); + + PackageMetadata metadata = PackageMetadata::fromLegacyJson(legacyJson, "getpkg.xyz"); + + ASSERT_STR_EQ("legacy-tool", metadata.name); + ASSERT_STR_EQ("2023.1201.1000", metadata.version); + ASSERT_STR_EQ("legacy123hash456", metadata.hash); + ASSERT_STR_EQ("x86_64", metadata.arch); + ASSERT_STR_EQ("getpkg.xyz", metadata.sourceServer); + ASSERT_NOT_EMPTY(metadata.installDate); // Should be auto-generated + + ASSERT_TRUE(metadata.isValid()); +} + +TEST(LegacyJsonMigrationWithCustomServer) { + nlohmann::json legacyJson = PackageMetadataTestHelper::createLegacyJson(); + + PackageMetadata metadata = PackageMetadata::fromLegacyJson(legacyJson, "custom.server.com"); + + ASSERT_STR_EQ("custom.server.com", metadata.sourceServer); + ASSERT_TRUE(metadata.isValid()); +} + +TEST(ValidationValid) { + PackageMetadata metadata = PackageMetadataTestHelper::createValidMetadata(); + + ASSERT_TRUE(metadata.isValid()); + ASSERT_TRUE(metadata.getValidationError().empty()); +} + +TEST(ValidationInvalidName) { + PackageMetadata metadata("", "2024.0115.1430", "abc123hash456", "x86_64", "getpkg.xyz"); + + ASSERT_FALSE(metadata.isValid()); + ASSERT_NOT_EMPTY(metadata.getValidationError()); +} + +TEST(ValidationInvalidVersion) { + PackageMetadata metadata("test-tool", "", "abc123hash456", "x86_64", "getpkg.xyz"); + + ASSERT_FALSE(metadata.isValid()); + ASSERT_NOT_EMPTY(metadata.getValidationError()); +} + +TEST(ValidationInvalidHash) { + PackageMetadata metadata("test-tool", "2024.0115.1430", "", "x86_64", "getpkg.xyz"); + + ASSERT_FALSE(metadata.isValid()); + ASSERT_NOT_EMPTY(metadata.getValidationError()); +} + +TEST(ValidationInvalidArch) { + PackageMetadata metadata("test-tool", "2024.0115.1430", "abc123hash456", "", "getpkg.xyz"); + + ASSERT_FALSE(metadata.isValid()); + ASSERT_NOT_EMPTY(metadata.getValidationError()); +} + +TEST(ValidationInvalidServer) { + PackageMetadata metadata("test-tool", "2024.0115.1430", "abc123hash456", "x86_64", ""); + + ASSERT_FALSE(metadata.isValid()); + ASSERT_NOT_EMPTY(metadata.getValidationError()); +} + +TEST(FileOperationsSaveAndLoad) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + auto filePath = tempDir / "test-metadata.json"; + + PackageMetadata original = PackageMetadataTestHelper::createValidMetadata(); + + // Save to file + bool saveResult = original.saveToFile(filePath); + ASSERT_TRUE(saveResult); + ASSERT_TRUE(std::filesystem::exists(filePath)); + + // Load from file + PackageMetadata loaded = PackageMetadata::loadFromFile(filePath); + + ASSERT_STR_EQ(original.name, loaded.name); + ASSERT_STR_EQ(original.version, loaded.version); + ASSERT_STR_EQ(original.hash, loaded.hash); + ASSERT_STR_EQ(original.arch, loaded.arch); + ASSERT_STR_EQ(original.sourceServer, loaded.sourceServer); + ASSERT_STR_EQ(original.installDate, loaded.installDate); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(FileOperationsInvalidPath) { + PackageMetadata metadata = PackageMetadataTestHelper::createValidMetadata(); + + // Try to save to invalid path + bool saveResult = metadata.saveToFile("/invalid/path/metadata.json"); + ASSERT_FALSE(saveResult); + + // Try to load from non-existent file + PackageMetadata loaded = PackageMetadata::loadFromFile("/nonexistent/file.json"); + ASSERT_FALSE(loaded.isValid()); +} + +TEST(NeedsUpdateComparison) { + PackageMetadata metadata = PackageMetadataTestHelper::createValidMetadata(); + + // Same hash - no update needed + ASSERT_FALSE(metadata.needsUpdate("abc123hash456")); + + // Different hash - update needed + ASSERT_TRUE(metadata.needsUpdate("different123hash456")); + + // Empty hash - update needed + ASSERT_TRUE(metadata.needsUpdate("")); +} + +TEST(TimestampGeneration) { + PackageMetadata metadata; + + std::string timestamp = metadata.getCurrentTimestamp(); + ASSERT_NOT_EMPTY(timestamp); + + // Should be in ISO format (basic check) + ASSERT_TRUE(timestamp.find("T") != std::string::npos); + ASSERT_TRUE(timestamp.find("Z") != std::string::npos); +} + +// PackageMetadataManager Tests + +TEST(ManagerDefaultConstructor) { + PackageMetadataManager manager; + + // Should initialize with default config directory + auto packagesDir = manager.getPackagesDirectory(); + ASSERT_FALSE(packagesDir.empty()); +} + +TEST(ManagerCustomConstructor) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + + auto packagesDir = manager.getPackagesDirectory(); + ASSERT_STR_EQ((tempDir / "packages").string(), packagesDir.string()); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerEnsurePackagesDirectory) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + + bool result = manager.ensurePackagesDirectory(); + ASSERT_TRUE(result); + + auto packagesDir = manager.getPackagesDirectory(); + ASSERT_TRUE(std::filesystem::exists(packagesDir)); + ASSERT_TRUE(std::filesystem::is_directory(packagesDir)); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerSaveAndLoadPackageMetadata) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + manager.ensurePackagesDirectory(); + + PackageMetadata metadata = PackageMetadataTestHelper::createValidMetadata(); + + // Save metadata + bool saveResult = manager.savePackageMetadata(metadata); + ASSERT_TRUE(saveResult); + + // Check if package exists + ASSERT_TRUE(manager.packageExists("test-tool")); + + // Load metadata + PackageMetadata loaded = manager.loadPackageMetadata("test-tool"); + ASSERT_TRUE(loaded.isValid()); + ASSERT_STR_EQ(metadata.name, loaded.name); + ASSERT_STR_EQ(metadata.version, loaded.version); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerRemovePackageMetadata) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + manager.ensurePackagesDirectory(); + + PackageMetadata metadata = PackageMetadataTestHelper::createValidMetadata(); + manager.savePackageMetadata(metadata); + + ASSERT_TRUE(manager.packageExists("test-tool")); + + bool removeResult = manager.removePackageMetadata("test-tool"); + ASSERT_TRUE(removeResult); + + ASSERT_FALSE(manager.packageExists("test-tool")); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerListInstalledPackages) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + manager.ensurePackagesDirectory(); + + // Save multiple packages + PackageMetadata metadata1("tool1", "1.0", "hash1", "x86_64", "server1"); + PackageMetadata metadata2("tool2", "2.0", "hash2", "aarch64", "server2"); + + manager.savePackageMetadata(metadata1); + manager.savePackageMetadata(metadata2); + + auto packages = manager.listInstalledPackages(); + ASSERT_EQ(2, packages.size()); + + // Should contain both tools (order may vary) + bool foundTool1 = std::find(packages.begin(), packages.end(), "tool1") != packages.end(); + bool foundTool2 = std::find(packages.begin(), packages.end(), "tool2") != packages.end(); + + ASSERT_TRUE(foundTool1); + ASSERT_TRUE(foundTool2); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerGetAllPackageMetadata) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + manager.ensurePackagesDirectory(); + + // Save multiple packages + PackageMetadata metadata1("tool1", "1.0", "hash1", "x86_64", "server1"); + PackageMetadata metadata2("tool2", "2.0", "hash2", "aarch64", "server2"); + + manager.savePackageMetadata(metadata1); + manager.savePackageMetadata(metadata2); + + auto allMetadata = manager.getAllPackageMetadata(); + ASSERT_EQ(2, allMetadata.size()); + + // Verify metadata content + bool foundTool1 = false, foundTool2 = false; + for (const auto& meta : allMetadata) { + if (meta.name == "tool1") { + foundTool1 = true; + ASSERT_STR_EQ("1.0", meta.version); + } else if (meta.name == "tool2") { + foundTool2 = true; + ASSERT_STR_EQ("2.0", meta.version); + } + } + + ASSERT_TRUE(foundTool1); + ASSERT_TRUE(foundTool2); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerMigrationFromLegacyFormat) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + // Create legacy package files in root config directory + auto legacyFile1 = tempDir / "legacy-tool1.json"; + auto legacyFile2 = tempDir / "legacy-tool2.json"; + + PackageMetadataTestHelper::createJsonFile(legacyFile1, PackageMetadataTestHelper::createLegacyJson()); + PackageMetadataTestHelper::createJsonFile(legacyFile2, nlohmann::json{ + {"name", "legacy-tool2"}, + {"version", "2023.1202.1100"}, + {"hash", "legacy789hash012"}, + {"arch", "aarch64"} + }); + + PackageMetadataManager manager(tempDir); + + // Find legacy files + auto legacyFiles = manager.findLegacyPackageFiles(); + ASSERT_EQ(2, legacyFiles.size()); + + // Perform migration + bool migrationResult = manager.migrateFromLegacyFormat(); + ASSERT_TRUE(migrationResult); + + // Verify packages directory was created + ASSERT_TRUE(std::filesystem::exists(manager.getPackagesDirectory())); + + // Verify packages were migrated + ASSERT_TRUE(manager.packageExists("legacy-tool1")); + ASSERT_TRUE(manager.packageExists("legacy-tool2")); + + // Verify metadata has server information + PackageMetadata migrated1 = manager.loadPackageMetadata("legacy-tool1"); + ASSERT_STR_EQ("getpkg.xyz", migrated1.sourceServer); + ASSERT_NOT_EMPTY(migrated1.installDate); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerValidateAllPackageMetadata) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + manager.ensurePackagesDirectory(); + + // Save valid and invalid metadata + PackageMetadata validMetadata = PackageMetadataTestHelper::createValidMetadata(); + PackageMetadata invalidMetadata("", "", "", "", ""); // All empty - invalid + + manager.savePackageMetadata(validMetadata); + + // Manually create invalid metadata file + auto invalidFile = manager.getPackagesDirectory() / "invalid-tool.json"; + PackageMetadataTestHelper::createJsonFile(invalidFile, PackageMetadataTestHelper::createCorruptedJson()); + + bool validationResult = manager.validateAllPackageMetadata(); + ASSERT_FALSE(validationResult); // Should fail due to invalid metadata + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +TEST(ManagerCleanupInvalidMetadata) { + auto tempDir = PackageMetadataTestHelper::createTempDir(); + + PackageMetadataManager manager(tempDir); + manager.ensurePackagesDirectory(); + + // Save valid metadata + PackageMetadata validMetadata = PackageMetadataTestHelper::createValidMetadata(); + manager.savePackageMetadata(validMetadata); + + // Create invalid metadata file + auto invalidFile = manager.getPackagesDirectory() / "invalid-tool.json"; + PackageMetadataTestHelper::createJsonFile(invalidFile, PackageMetadataTestHelper::createCorruptedJson()); + + // Should have 2 files initially + auto packagesBefore = manager.listInstalledPackages(); + ASSERT_EQ(2, packagesBefore.size()); + + // Cleanup invalid metadata + int cleanedCount = manager.cleanupInvalidMetadata(); + ASSERT_EQ(1, cleanedCount); + + // Should have 1 file after cleanup + auto packagesAfter = manager.listInstalledPackages(); + ASSERT_EQ(1, packagesAfter.size()); + ASSERT_STR_EQ("test-tool", packagesAfter[0]); + + PackageMetadataTestHelper::cleanupTempDir(tempDir); +} + +// Registration function +void registerPackageMetadataTests() { + register_PackageMetadata_DefaultConstructor(); + register_PackageMetadata_ParameterizedConstructor(); + register_PackageMetadata_JsonSerialization(); + register_PackageMetadata_JsonDeserialization(); + register_PackageMetadata_LegacyJsonMigration(); + register_PackageMetadata_LegacyJsonMigrationWithCustomServer(); + register_PackageMetadata_ValidationValid(); + register_PackageMetadata_ValidationInvalidName(); + register_PackageMetadata_ValidationInvalidVersion(); + register_PackageMetadata_ValidationInvalidHash(); + register_PackageMetadata_ValidationInvalidArch(); + register_PackageMetadata_ValidationInvalidServer(); + register_PackageMetadata_FileOperationsSaveAndLoad(); + register_PackageMetadata_FileOperationsInvalidPath(); + register_PackageMetadata_NeedsUpdateComparison(); + register_PackageMetadata_TimestampGeneration(); + register_PackageMetadata_ManagerDefaultConstructor(); + register_PackageMetadata_ManagerCustomConstructor(); + register_PackageMetadata_ManagerEnsurePackagesDirectory(); + register_PackageMetadata_ManagerSaveAndLoadPackageMetadata(); + register_PackageMetadata_ManagerRemovePackageMetadata(); + register_PackageMetadata_ManagerListInstalledPackages(); + register_PackageMetadata_ManagerGetAllPackageMetadata(); + register_PackageMetadata_ManagerMigrationFromLegacyFormat(); + register_PackageMetadata_ManagerValidateAllPackageMetadata(); + register_PackageMetadata_ManagerCleanupInvalidMetadata(); +} \ No newline at end of file diff --git a/getpkg/test/test_server_manager.cpp b/getpkg/test/test_server_manager.cpp new file mode 100644 index 0000000..42a9465 --- /dev/null +++ b/getpkg/test/test_server_manager.cpp @@ -0,0 +1,306 @@ +#include "test_framework.hpp" +#include "ServerManager.hpp" +#include +#include +#include + +// Test helper class for ServerManager testing +class ServerManagerTestHelper { +public: + static std::filesystem::path createTempConfigDir() { + auto tempDir = std::filesystem::temp_directory_path() / "getpkg_test" / std::to_string(std::time(nullptr)); + std::filesystem::create_directories(tempDir); + return tempDir; + } + + static void cleanupTempDir(const std::filesystem::path& dir) { + if (std::filesystem::exists(dir)) { + std::filesystem::remove_all(dir); + } + } + + static void createLegacyTokenFile(const std::filesystem::path& configDir, const std::string& token) { + auto legacyDir = configDir / "getpkg.xyz"; + std::filesystem::create_directories(legacyDir); + + std::ofstream tokenFile(legacyDir / "write_token.txt"); + tokenFile << token; + tokenFile.close(); + } + + static void createCorruptedConfigFile(const std::filesystem::path& configDir) { + std::ofstream configFile(configDir / "servers.json"); + configFile << "{ invalid json content"; + configFile.close(); + } +}; + +TEST(ServerManager, DefaultConfiguration) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + + // Set environment variable to use temp directory + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + + auto servers = manager.getServers(); + ASSERT_EQ(1, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + ASSERT_STR_EQ("getpkg.xyz", manager.getDefaultServer()); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ServerManager, AddValidServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + + auto result = manager.addServer("packages.example.com"); + ASSERT_EQ(ServerManagerError::None, result); + + auto servers = manager.getServers(); + ASSERT_EQ(2, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + ASSERT_STR_EQ("packages.example.com", servers[1]); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ServerManager, AddInvalidServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + + // Test invalid URLs + auto result1 = manager.addServer("not-a-url"); + ASSERT_EQ(ServerManagerError::InvalidUrl, result1); + + auto result2 = manager.addServer(""); + ASSERT_EQ(ServerManagerError::InvalidUrl, result2); + + auto result3 = manager.addServer("ftp://invalid-protocol.com"); + ASSERT_EQ(ServerManagerError::InvalidUrl, result3); + + // Should still have only default server + auto servers = manager.getServers(); + ASSERT_EQ(1, servers.size()); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ServerManager, AddDuplicateServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + + auto result1 = manager.addServer("packages.example.com"); + ASSERT_EQ(ServerManagerError::None, result1); + + auto result2 = manager.addServer("packages.example.com"); + ASSERT_EQ(ServerManagerError::ServerAlreadyExists, result2); + + auto servers = manager.getServers(); + ASSERT_EQ(2, servers.size()); // Should not add duplicate + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(RemoveServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + manager.addServer("packages.example.com"); + manager.addServer("test.server.com"); + + auto result = manager.removeServer("packages.example.com"); + ASSERT_EQ(ServerManagerError::None, result); + + auto servers = manager.getServers(); + ASSERT_EQ(2, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + ASSERT_STR_EQ("test.server.com", servers[1]); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(RemoveNonExistentServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + + auto result = manager.removeServer("nonexistent.server.com"); + ASSERT_EQ(ServerManagerError::ServerNotFound, result); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(RemoveLastServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + + auto result = manager.removeServer("getpkg.xyz"); + ASSERT_EQ(ServerManagerError::LastServerRemoval, result); + + auto servers = manager.getServers(); + ASSERT_EQ(1, servers.size()); // Should still have the server + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(WriteTokenManagement) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + manager.addServer("packages.example.com"); + + // Set write token + auto result = manager.setWriteToken("packages.example.com", "test-token-123"); + ASSERT_EQ(ServerManagerError::None, result); + + // Verify token + ASSERT_TRUE(manager.hasWriteToken("packages.example.com")); + ASSERT_STR_EQ("test-token-123", manager.getWriteToken("packages.example.com")); + + // Test servers with tokens + auto serversWithTokens = manager.getServersWithTokens(); + ASSERT_EQ(1, serversWithTokens.size()); + ASSERT_STR_EQ("packages.example.com", serversWithTokens[0]); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(DefaultPublishServer) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + ServerManager manager; + manager.ensureDefaultConfiguration(); + manager.addServer("packages.example.com"); + manager.addServer("test.server.com"); + + // No tokens initially + ASSERT_STR_EQ("", manager.getDefaultPublishServer()); + + // Add token to second server + manager.setWriteToken("test.server.com", "token2"); + ASSERT_STR_EQ("test.server.com", manager.getDefaultPublishServer()); + + // Add token to first server - should become default + manager.setWriteToken("getpkg.xyz", "token1"); + ASSERT_STR_EQ("getpkg.xyz", manager.getDefaultPublishServer()); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ConfigurationPersistence) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + { + ServerManager manager1; + manager1.ensureDefaultConfiguration(); + manager1.addServer("packages.example.com"); + manager1.setWriteToken("packages.example.com", "test-token"); + manager1.saveConfiguration(); + } + + { + ServerManager manager2; + auto result = manager2.loadConfiguration(); + ASSERT_EQ(ServerManagerError::None, result); + + auto servers = manager2.getServers(); + ASSERT_EQ(2, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + ASSERT_STR_EQ("packages.example.com", servers[1]); + + ASSERT_TRUE(manager2.hasWriteToken("packages.example.com")); + ASSERT_STR_EQ("test-token", manager2.getWriteToken("packages.example.com")); + } + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(CorruptedConfigurationRecovery) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + // Create corrupted config file + ServerManagerTestHelper::createCorruptedConfigFile(tempDir / ".config" / "getpkg"); + + ServerManager manager; + auto result = manager.loadConfiguration(); + + // Should recover by creating default configuration + ASSERT_EQ(ServerManagerError::None, result); + + auto servers = manager.getServers(); + ASSERT_EQ(1, servers.size()); + ASSERT_STR_EQ("getpkg.xyz", servers[0]); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(LegacyMigration) { + auto tempDir = ServerManagerTestHelper::createTempConfigDir(); + setenv("HOME", tempDir.c_str(), 1); + + auto configDir = tempDir / ".config" / "getpkg"; + std::filesystem::create_directories(configDir); + + // Create legacy token file + ServerManagerTestHelper::createLegacyTokenFile(configDir, "legacy-token-123"); + + ServerManager manager; + bool migrated = manager.migrateFromLegacy(); + ASSERT_TRUE(migrated); + + // Verify migration + ASSERT_TRUE(manager.hasWriteToken("getpkg.xyz")); + ASSERT_STR_EQ("legacy-token-123", manager.getWriteToken("getpkg.xyz")); + + ServerManagerTestHelper::cleanupTempDir(tempDir); +} + +TEST(ErrorMessages) { + ServerManager manager; + + ASSERT_NOT_EMPTY(manager.getErrorMessage(ServerManagerError::InvalidUrl)); + ASSERT_NOT_EMPTY(manager.getErrorMessage(ServerManagerError::ServerNotFound)); + ASSERT_NOT_EMPTY(manager.getErrorMessage(ServerManagerError::ServerAlreadyExists)); + ASSERT_NOT_EMPTY(manager.getErrorMessage(ServerManagerError::LastServerRemoval)); +} + +// Registration function +void registerServerManagerTests() { + register_ServerManager_DefaultConfiguration(); + register_ServerManager_AddValidServer(); + register_ServerManager_AddInvalidServer(); + register_ServerManager_AddDuplicateServer(); + register_ServerManager_RemoveServer(); + register_ServerManager_RemoveNonExistentServer(); + register_ServerManager_RemoveLastServer(); + register_ServerManager_WriteTokenManagement(); + register_ServerManager_DefaultPublishServer(); + register_ServerManager_ConfigurationPersistence(); + register_ServerManager_CorruptedConfigurationRecovery(); + register_ServerManager_LegacyMigration(); + register_ServerManager_ErrorMessages(); +} \ No newline at end of file