test: Add 11 and update 3 files
Some checks failed
Build-Test-Publish / build (linux/amd64) (push) Successful in 1m4s
Build-Test-Publish / build (linux/arm64) (push) Failing after 1m51s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Has been skipped
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Has been skipped

This commit is contained in:
Your Name
2025-07-22 22:09:55 +12:00
parent 507897d9a1
commit 501fa65d76
14 changed files with 2740 additions and 39 deletions

168
ARCHITECTURE.md Normal file
View File

@ -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/<tool_name>/`
- **Executable symlinks**: `~/.local/bin/getpkg/`
- **Configuration**: `~/.config/getpkg/`
- `packages/<tool_name>.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
```

View File

@ -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/<tool_name>/`
- 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/<tool_name>/` 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/<tool_name>/`
- **Executable symlinks**: `~/.local/bin/getpkg/` (added to PATH)
- **Package metadata**: `~/.config/getpkg/packages/<tool_name>.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.
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`

View File

@ -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;

View File

@ -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=$?

View File

@ -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)

View File

@ -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"]

View File

@ -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!"

View File

@ -0,0 +1,91 @@
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <functional>
#include <stdexcept>
// Simple test framework
class TestRunner {
public:
using TestFunction = std::function<void()>;
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<std::pair<std::string, TestFunction>> 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()

View File

@ -0,0 +1,343 @@
#include "GetbinClient.hpp"
#include <filesystem>
#include <fstream>
#include <thread>
#include <chrono>
// Test framework declarations from test_main.cpp
class TestRunner {
public:
static TestRunner& instance();
void addTest(const std::string& name, std::function<void()> 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<char>(file)), std::istreambuf_iterator<char>());
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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::pair<std::string, std::vector<std::string>>> 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<std::string> 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();
}

View File

@ -0,0 +1,530 @@
#include "ServerManager.hpp"
#include "GetbinClient.hpp"
#include "PackageMetadata.hpp"
#include "MigrationManager.hpp"
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
// Test framework declarations from test_main.cpp
class TestRunner {
public:
static TestRunner& instance();
void addTest(const std::string& name, std::function<void()> 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<std::string> 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();
}

18
getpkg/test/test_main.cpp Normal file
View File

@ -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;
}

View File

@ -0,0 +1,545 @@
#include "MigrationManager.hpp"
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
// Test framework declarations from test_main.cpp
class TestRunner {
public:
static TestRunner& instance();
void addTest(const std::string& name, std::function<void()> 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();
}

View File

@ -0,0 +1,545 @@
#include "PackageMetadata.hpp"
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
// Test framework declarations from test_main.cpp
class TestRunner {
public:
static TestRunner& instance();
void addTest(const std::string& name, std::function<void()> 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();
}

View File

@ -0,0 +1,306 @@
#include "test_framework.hpp"
#include "ServerManager.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
// 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();
}