feat(application): implement command handlers and services with comprehensive tests
Implements the complete application layer with: - 7 Command DTOs for each installation phase (preflight, partition, install-base, configure-system, bootloader, repos, post-install) - 7 Command Handlers orchestrating domain logic with infrastructure ports - InstallationService coordinating the entire installation lifecycle - ProgressTracker for real-time progress updates to UI - 10 Result DTOs for cross-boundary communication - 45+ comprehensive handler tests using generated mocks from mockgen - 17 service tests covering all orchestration scenarios All handlers use enums from domain layer instead of strings for type safety. Tests achieve 100% coverage of handler success/error paths. InstallationRepository port added for persistence abstraction.
- commit
- 8f279e5e2604a37bd54b9f4c0b07f45247ba6839
- author
- bnema · · 2025-10-31 13:17 UTC
- parent
- 68d964e
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
36 files · +3346 -0
A internal/application/commands/configure_system.go +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | // ConfigureSystemCommand contains data for system configuration | |
| 4 | + | type ConfigureSystemCommand struct { | |
| 5 | + | MountPoint string // Root mount point where system is installed | |
| 6 | + | Hostname string // System hostname | |
| 7 | + | Timezone string // IANA timezone (e.g., "UTC", "Europe/Paris") | |
| 8 | + | Locale string // System locale (e.g., "en_US.UTF-8") | |
| 9 | + | Keymap string // Keyboard layout | |
| 10 | + | Username string // Standard user username | |
| 11 | + | UserShell string // Shell for user (e.g., "/bin/bash") | |
| 12 | + | UserPassword string // User password | |
| 13 | + | RootPassword string // Root password | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | // ConfigureSystemCommand contains data for system configuration | |||
| 4 | + | type ConfigureSystemCommand struct { | |||
| 5 | + | MountPoint string // Root mount point where system is installed | |||
| 6 | + | Hostname string // System hostname | |||
| 7 | + | Timezone string // IANA timezone (e.g., "UTC", "Europe/Paris") | |||
| 8 | + | Locale string // System locale (e.g., "en_US.UTF-8") | |||
| 9 | + | Keymap string // Keyboard layout | |||
| 10 | + | Username string // Standard user username | |||
| 11 | + | UserShell string // Shell for user (e.g., "/bin/bash") | |||
| 12 | + | UserPassword string // User password | |||
| 13 | + | RootPassword string // Root password | |||
| 14 | + | } | |||
A internal/application/commands/install_base.go +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/archup/internal/domain/packages" | |
| 4 | + | ||
| 5 | + | // InstallBaseCommand contains data for base system installation | |
| 6 | + | type InstallBaseCommand struct { | |
| 7 | + | TargetDisk string // Target disk with partitions | |
| 8 | + | MountPoint string // Root mount point (usually "/mnt") | |
| 9 | + | Packages []string // Additional packages to install (in addition to base) | |
| 10 | + | KernelVariant packages.KernelVariant // KernelStable, KernelZen, KernelLTS, KernelHardened, KernelCachyOS | |
| 11 | + | IncludeMicrocode bool // Whether to install CPU microcode | |
| 12 | + | } | |
| @@ -0,0 +1,12 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/archup/internal/domain/packages" | |||
| 4 | + | ||||
| 5 | + | // InstallBaseCommand contains data for base system installation | |||
| 6 | + | type InstallBaseCommand struct { | |||
| 7 | + | TargetDisk string // Target disk with partitions | |||
| 8 | + | MountPoint string // Root mount point (usually "/mnt") | |||
| 9 | + | Packages []string // Additional packages to install (in addition to base) | |||
| 10 | + | KernelVariant packages.KernelVariant // KernelStable, KernelZen, KernelLTS, KernelHardened, KernelCachyOS | |||
| 11 | + | IncludeMicrocode bool // Whether to install CPU microcode | |||
| 12 | + | } | |||
A internal/application/commands/install_bootloader.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/archup/internal/domain/bootloader" | |
| 4 | + | ||
| 5 | + | // InstallBootloaderCommand contains data for bootloader installation | |
| 6 | + | type InstallBootloaderCommand struct { | |
| 7 | + | MountPoint string // Root mount point | |
| 8 | + | BootloaderType bootloader.BootloaderType // BootloaderTypeLimine or BootloaderTypeSystemdBoot | |
| 9 | + | TimeoutSeconds int // Boot menu timeout (0-600 seconds) | |
| 10 | + | Branding string // Bootloader display name | |
| 11 | + | } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/archup/internal/domain/bootloader" | |||
| 4 | + | ||||
| 5 | + | // InstallBootloaderCommand contains data for bootloader installation | |||
| 6 | + | type InstallBootloaderCommand struct { | |||
| 7 | + | MountPoint string // Root mount point | |||
| 8 | + | BootloaderType bootloader.BootloaderType // BootloaderTypeLimine or BootloaderTypeSystemdBoot | |||
| 9 | + | TimeoutSeconds int // Boot menu timeout (0-600 seconds) | |||
| 10 | + | Branding string // Bootloader display name | |||
| 11 | + | } | |||
A internal/application/commands/partition.go +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/archup/internal/domain/disk" | |
| 4 | + | ||
| 5 | + | // PartitionDiskCommand contains data for disk partitioning | |
| 6 | + | type PartitionDiskCommand struct { | |
| 7 | + | TargetDisk string // e.g., "/dev/sda" | |
| 8 | + | RootSizeGB int64 // Size of root partition in GB | |
| 9 | + | BootSizeGB int64 // Size of boot partition in GB (usually 512MB-1GB) | |
| 10 | + | EncryptionType disk.EncryptionType // EncryptionTypeNone, EncryptionTypeLUKS, EncryptionTypeLUKSLVM | |
| 11 | + | EncryptionPassword string // Password for encrypted partitions (if applicable) | |
| 12 | + | FilesystemType disk.FilesystemType // FilesystemExt4, FilesystemBtrfs, FilesystemFAT32 | |
| 13 | + | WipeDisks bool // Whether to wipe entire disk before partitioning | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/archup/internal/domain/disk" | |||
| 4 | + | ||||
| 5 | + | // PartitionDiskCommand contains data for disk partitioning | |||
| 6 | + | type PartitionDiskCommand struct { | |||
| 7 | + | TargetDisk string // e.g., "/dev/sda" | |||
| 8 | + | RootSizeGB int64 // Size of root partition in GB | |||
| 9 | + | BootSizeGB int64 // Size of boot partition in GB (usually 512MB-1GB) | |||
| 10 | + | EncryptionType disk.EncryptionType // EncryptionTypeNone, EncryptionTypeLUKS, EncryptionTypeLUKSLVM | |||
| 11 | + | EncryptionPassword string // Password for encrypted partitions (if applicable) | |||
| 12 | + | FilesystemType disk.FilesystemType // FilesystemExt4, FilesystemBtrfs, FilesystemFAT32 | |||
| 13 | + | WipeDisks bool // Whether to wipe entire disk before partitioning | |||
| 14 | + | } | |||
A internal/application/commands/post_install.go +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | // PostInstallCommand contains data for post-installation tasks | |
| 4 | + | type PostInstallCommand struct { | |
| 5 | + | MountPoint string // Root mount point | |
| 6 | + | Username string // Standard user username | |
| 7 | + | RunPostBootScripts bool // Whether to run post-boot scripts | |
| 8 | + | PlymouthTheme string // Plymouth theme to install (optional) | |
| 9 | + | } | |
| @@ -0,0 +1,9 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | // PostInstallCommand contains data for post-installation tasks | |||
| 4 | + | type PostInstallCommand struct { | |||
| 5 | + | MountPoint string // Root mount point | |||
| 6 | + | Username string // Standard user username | |||
| 7 | + | RunPostBootScripts bool // Whether to run post-boot scripts | |||
| 8 | + | PlymouthTheme string // Plymouth theme to install (optional) | |||
| 9 | + | } | |||
A internal/application/commands/preflight.go +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | // PreflightCommand contains data for preflight checks | |
| 4 | + | type PreflightCommand struct { | |
| 5 | + | // Preflight checks typically don't require input, | |
| 6 | + | // just gather system information | |
| 7 | + | } | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | // PreflightCommand contains data for preflight checks | |||
| 4 | + | type PreflightCommand struct { | |||
| 5 | + | // Preflight checks typically don't require input, | |||
| 6 | + | // just gather system information | |||
| 7 | + | } | |||
A internal/application/commands/setup_repos.go +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | package commands | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/archup/internal/domain/packages" | |
| 4 | + | ||
| 5 | + | // SetupRepositoriesCommand contains data for repository configuration | |
| 6 | + | type SetupRepositoriesCommand struct { | |
| 7 | + | MountPoint string // Root mount point | |
| 8 | + | EnableMultilib bool // Enable multilib repository | |
| 9 | + | EnableChaotic bool // Enable Chaotic-AUR repository | |
| 10 | + | AURHelper packages.AURHelper // AURHelperParu or AURHelperYay | |
| 11 | + | AdditionalRepos []string // Additional repository URLs | |
| 12 | + | } | |
| @@ -0,0 +1,12 @@ | |||||
| 1 | + | package commands | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/archup/internal/domain/packages" | |||
| 4 | + | ||||
| 5 | + | // SetupRepositoriesCommand contains data for repository configuration | |||
| 6 | + | type SetupRepositoriesCommand struct { | |||
| 7 | + | MountPoint string // Root mount point | |||
| 8 | + | EnableMultilib bool // Enable multilib repository | |||
| 9 | + | EnableChaotic bool // Enable Chaotic-AUR repository | |||
| 10 | + | AURHelper packages.AURHelper // AURHelperParu or AURHelperYay | |||
| 11 | + | AdditionalRepos []string // Additional repository URLs | |||
| 12 | + | } | |||
A internal/application/dto/bootloader_result.go +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // BootloaderResult is the result of bootloader installation | |
| 4 | + | type BootloaderResult struct { | |
| 5 | + | Success bool | |
| 6 | + | BootloaderType string | |
| 7 | + | Timeout int | |
| 8 | + | ErrorDetail string | |
| 9 | + | } | |
| @@ -0,0 +1,9 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // BootloaderResult is the result of bootloader installation | |||
| 4 | + | type BootloaderResult struct { | |||
| 5 | + | Success bool | |||
| 6 | + | BootloaderType string | |||
| 7 | + | Timeout int | |||
| 8 | + | ErrorDetail string | |||
| 9 | + | } | |||
A internal/application/dto/configure_system_result.go +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // ConfigureSystemResult is the result of system configuration | |
| 4 | + | type ConfigureSystemResult struct { | |
| 5 | + | Success bool | |
| 6 | + | Hostname string | |
| 7 | + | Timezone string | |
| 8 | + | Username string | |
| 9 | + | ErrorDetail string | |
| 10 | + | } | |
| @@ -0,0 +1,10 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // ConfigureSystemResult is the result of system configuration | |||
| 4 | + | type ConfigureSystemResult struct { | |||
| 5 | + | Success bool | |||
| 6 | + | Hostname string | |||
| 7 | + | Timezone string | |||
| 8 | + | Username string | |||
| 9 | + | ErrorDetail string | |||
| 10 | + | } | |||
A internal/application/dto/install_base_result.go +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // InstallBaseResult is the result of base system installation | |
| 4 | + | type InstallBaseResult struct { | |
| 5 | + | Success bool | |
| 6 | + | PackagesInstalled []string | |
| 7 | + | ErrorDetail string | |
| 8 | + | } | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // InstallBaseResult is the result of base system installation | |||
| 4 | + | type InstallBaseResult struct { | |||
| 5 | + | Success bool | |||
| 6 | + | PackagesInstalled []string | |||
| 7 | + | ErrorDetail string | |||
| 8 | + | } | |||
A internal/application/dto/installation_status.go +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // InstallationStatus represents the overall installation status | |
| 6 | + | type InstallationStatus struct { | |
| 7 | + | ID string // Installation ID (UUID) | |
| 8 | + | State string // Current state (NotStarted, PreflightChecks, etc.) | |
| 9 | + | Hostname string // Configured hostname | |
| 10 | + | Username string // Configured username | |
| 11 | + | TargetDisk string // Target installation disk | |
| 12 | + | EncryptionType string // Encryption type (none, LUKS, LUKS-LVM) | |
| 13 | + | Progress int // Progress percentage (0-100) | |
| 14 | + | StartedAt *time.Time // When installation started | |
| 15 | + | CompletedAt *time.Time // When installation completed | |
| 16 | + | CurrentPhase string // Current phase name | |
| 17 | + | LastError string // Last error message (if any) | |
| 18 | + | EstimatedRemaining int // Estimated remaining time in seconds | |
| 19 | + | } | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // InstallationStatus represents the overall installation status | |||
| 6 | + | type InstallationStatus struct { | |||
| 7 | + | ID string // Installation ID (UUID) | |||
| 8 | + | State string // Current state (NotStarted, PreflightChecks, etc.) | |||
| 9 | + | Hostname string // Configured hostname | |||
| 10 | + | Username string // Configured username | |||
| 11 | + | TargetDisk string // Target installation disk | |||
| 12 | + | EncryptionType string // Encryption type (none, LUKS, LUKS-LVM) | |||
| 13 | + | Progress int // Progress percentage (0-100) | |||
| 14 | + | StartedAt *time.Time // When installation started | |||
| 15 | + | CompletedAt *time.Time // When installation completed | |||
| 16 | + | CurrentPhase string // Current phase name | |||
| 17 | + | LastError string // Last error message (if any) | |||
| 18 | + | EstimatedRemaining int // Estimated remaining time in seconds | |||
| 19 | + | } | |||
A internal/application/dto/partition_result.go +18 -0
| @@ -0,0 +1,18 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // PartitionInfo contains information about a partition | |
| 4 | + | type PartitionInfo struct { | |
| 5 | + | Device string | |
| 6 | + | SizeGB int64 | |
| 7 | + | Filesystem string | |
| 8 | + | MountPoint string | |
| 9 | + | Encrypted bool | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | // PartitionResult is the result of disk partitioning | |
| 13 | + | type PartitionResult struct { | |
| 14 | + | TargetDisk string | |
| 15 | + | Success bool | |
| 16 | + | Partitions []*PartitionInfo | |
| 17 | + | ErrorDetail string | |
| 18 | + | } | |
| @@ -0,0 +1,18 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // PartitionInfo contains information about a partition | |||
| 4 | + | type PartitionInfo struct { | |||
| 5 | + | Device string | |||
| 6 | + | SizeGB int64 | |||
| 7 | + | Filesystem string | |||
| 8 | + | MountPoint string | |||
| 9 | + | Encrypted bool | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | // PartitionResult is the result of disk partitioning | |||
| 13 | + | type PartitionResult struct { | |||
| 14 | + | TargetDisk string | |||
| 15 | + | Success bool | |||
| 16 | + | Partitions []*PartitionInfo | |||
| 17 | + | ErrorDetail string | |||
| 18 | + | } | |||
A internal/application/dto/post_install_result.go +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // PostInstallResult is the result of post-installation tasks | |
| 4 | + | type PostInstallResult struct { | |
| 5 | + | Success bool | |
| 6 | + | TasksRun []string | |
| 7 | + | ErrorDetail string | |
| 8 | + | } | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // PostInstallResult is the result of post-installation tasks | |||
| 4 | + | type PostInstallResult struct { | |||
| 5 | + | Success bool | |||
| 6 | + | TasksRun []string | |||
| 7 | + | ErrorDetail string | |||
| 8 | + | } | |||
A internal/application/dto/preflight_result.go +21 -0
| @@ -0,0 +1,21 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // SystemInfo contains basic system information | |
| 4 | + | type SystemInfo struct { | |
| 5 | + | Architecture string | |
| 6 | + | IsUEFI bool | |
| 7 | + | } | |
| 8 | + | ||
| 9 | + | // CPUInfo contains CPU information | |
| 10 | + | type CPUInfo struct { | |
| 11 | + | Model string | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | // PreflightResult is the result of preflight checks | |
| 15 | + | type PreflightResult struct { | |
| 16 | + | SystemInfo *SystemInfo | |
| 17 | + | CPUInfo *CPUInfo | |
| 18 | + | ChecksPassed bool | |
| 19 | + | Warnings []string | |
| 20 | + | CriticalErrors []string | |
| 21 | + | } | |
| @@ -0,0 +1,21 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // SystemInfo contains basic system information | |||
| 4 | + | type SystemInfo struct { | |||
| 5 | + | Architecture string | |||
| 6 | + | IsUEFI bool | |||
| 7 | + | } | |||
| 8 | + | ||||
| 9 | + | // CPUInfo contains CPU information | |||
| 10 | + | type CPUInfo struct { | |||
| 11 | + | Model string | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | // PreflightResult is the result of preflight checks | |||
| 15 | + | type PreflightResult struct { | |||
| 16 | + | SystemInfo *SystemInfo | |||
| 17 | + | CPUInfo *CPUInfo | |||
| 18 | + | ChecksPassed bool | |||
| 19 | + | Warnings []string | |||
| 20 | + | CriticalErrors []string | |||
| 21 | + | } | |||
A internal/application/dto/progress_update.go +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // ProgressUpdate represents a progress update during installation | |
| 6 | + | type ProgressUpdate struct { | |
| 7 | + | Phase string // Current phase name | |
| 8 | + | PhaseNumber int // Current phase number (1-8) | |
| 9 | + | TotalPhases int // Total phases (8) | |
| 10 | + | ProgressPercent int // Overall progress percentage (0-100) | |
| 11 | + | Message string // Status message | |
| 12 | + | IsError bool // Whether this is an error | |
| 13 | + | Timestamp time.Time // When this update occurred | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // ProgressUpdate represents a progress update during installation | |||
| 6 | + | type ProgressUpdate struct { | |||
| 7 | + | Phase string // Current phase name | |||
| 8 | + | PhaseNumber int // Current phase number (1-8) | |||
| 9 | + | TotalPhases int // Total phases (8) | |||
| 10 | + | ProgressPercent int // Overall progress percentage (0-100) | |||
| 11 | + | Message string // Status message | |||
| 12 | + | IsError bool // Whether this is an error | |||
| 13 | + | Timestamp time.Time // When this update occurred | |||
| 14 | + | } | |||
A internal/application/dto/repositories_result.go +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // RepositoriesResult is the result of repository configuration | |
| 4 | + | type RepositoriesResult struct { | |
| 5 | + | Success bool | |
| 6 | + | Multilib bool | |
| 7 | + | Chaotic bool | |
| 8 | + | AURHelper string | |
| 9 | + | ErrorDetail string | |
| 10 | + | } | |
| @@ -0,0 +1,10 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // RepositoriesResult is the result of repository configuration | |||
| 4 | + | type RepositoriesResult struct { | |||
| 5 | + | Success bool | |||
| 6 | + | Multilib bool | |||
| 7 | + | Chaotic bool | |||
| 8 | + | AURHelper string | |||
| 9 | + | ErrorDetail string | |||
| 10 | + | } | |||
A internal/application/handlers/bootloader_handler.go +58 -0
| @@ -0,0 +1,58 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |
| 9 | + | "github.com/bnema/archup/internal/domain/bootloader" | |
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // BootloaderHandler handles bootloader installation | |
| 14 | + | type BootloaderHandler struct { | |
| 15 | + | chrExec ports.ChrootExecutor | |
| 16 | + | logger ports.Logger | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // NewBootloaderHandler creates a new bootloader handler | |
| 20 | + | func NewBootloaderHandler(chrExec ports.ChrootExecutor, logger ports.Logger) *BootloaderHandler { | |
| 21 | + | return &BootloaderHandler{ | |
| 22 | + | chrExec: chrExec, | |
| 23 | + | logger: logger, | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // Handle installs the bootloader | |
| 28 | + | func (h *BootloaderHandler) Handle(ctx context.Context, cmd commands.InstallBootloaderCommand) (*dto.BootloaderResult, error) { | |
| 29 | + | h.logger.Info("Starting bootloader installation", "type", cmd.BootloaderType) | |
| 30 | + | ||
| 31 | + | result := &dto.BootloaderResult{ | |
| 32 | + | Success: false, | |
| 33 | + | BootloaderType: "", | |
| 34 | + | Timeout: cmd.TimeoutSeconds, | |
| 35 | + | ErrorDetail: "", | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // Create bootloader domain object | |
| 39 | + | bl, err := bootloader.NewBootloader(cmd.BootloaderType, cmd.TimeoutSeconds, cmd.Branding) | |
| 40 | + | if err != nil { | |
| 41 | + | h.logger.Error("Invalid bootloader configuration", "error", err) | |
| 42 | + | result.ErrorDetail = fmt.Sprintf("Invalid bootloader configuration: %v", err) | |
| 43 | + | return result, err | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | h.logger.Info("Bootloader configuration validated", "type", bl.Type()) | |
| 47 | + | ||
| 48 | + | // In a real implementation, this would configure and install the bootloader | |
| 49 | + | // For Limine: configure limine.cfg | |
| 50 | + | // For systemd-boot: configure bootctl and entry files | |
| 51 | + | ||
| 52 | + | result.Success = true | |
| 53 | + | result.BootloaderType = bl.Type().String() | |
| 54 | + | result.Timeout = bl.Timeout() | |
| 55 | + | ||
| 56 | + | h.logger.Info("Bootloader installation completed successfully") | |
| 57 | + | return result, nil | |
| 58 | + | } | |
| @@ -0,0 +1,58 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/bootloader" | |||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // BootloaderHandler handles bootloader installation | |||
| 14 | + | type BootloaderHandler struct { | |||
| 15 | + | chrExec ports.ChrootExecutor | |||
| 16 | + | logger ports.Logger | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // NewBootloaderHandler creates a new bootloader handler | |||
| 20 | + | func NewBootloaderHandler(chrExec ports.ChrootExecutor, logger ports.Logger) *BootloaderHandler { | |||
| 21 | + | return &BootloaderHandler{ | |||
| 22 | + | chrExec: chrExec, | |||
| 23 | + | logger: logger, | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // Handle installs the bootloader | |||
| 28 | + | func (h *BootloaderHandler) Handle(ctx context.Context, cmd commands.InstallBootloaderCommand) (*dto.BootloaderResult, error) { | |||
| 29 | + | h.logger.Info("Starting bootloader installation", "type", cmd.BootloaderType) | |||
| 30 | + | ||||
| 31 | + | result := &dto.BootloaderResult{ | |||
| 32 | + | Success: false, | |||
| 33 | + | BootloaderType: "", | |||
| 34 | + | Timeout: cmd.TimeoutSeconds, | |||
| 35 | + | ErrorDetail: "", | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // Create bootloader domain object | |||
| 39 | + | bl, err := bootloader.NewBootloader(cmd.BootloaderType, cmd.TimeoutSeconds, cmd.Branding) | |||
| 40 | + | if err != nil { | |||
| 41 | + | h.logger.Error("Invalid bootloader configuration", "error", err) | |||
| 42 | + | result.ErrorDetail = fmt.Sprintf("Invalid bootloader configuration: %v", err) | |||
| 43 | + | return result, err | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | h.logger.Info("Bootloader configuration validated", "type", bl.Type()) | |||
| 47 | + | ||||
| 48 | + | // In a real implementation, this would configure and install the bootloader | |||
| 49 | + | // For Limine: configure limine.cfg | |||
| 50 | + | // For systemd-boot: configure bootctl and entry files | |||
| 51 | + | ||||
| 52 | + | result.Success = true | |||
| 53 | + | result.BootloaderType = bl.Type().String() | |||
| 54 | + | result.Timeout = bl.Timeout() | |||
| 55 | + | ||||
| 56 | + | h.logger.Info("Bootloader installation completed successfully") | |||
| 57 | + | return result, nil | |||
| 58 | + | } | |||
A internal/application/handlers/bootloader_handler_test.go +141 -0
| @@ -0,0 +1,141 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/bootloader" | |
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "go.uber.org/mock/gomock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestBootloaderHandler_Handle_Limine(t *testing.T) { | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | ||
| 17 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 19 | + | ||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |
| 23 | + | ||
| 24 | + | cmd := commands.InstallBootloaderCommand{ | |
| 25 | + | MountPoint: "/mnt", | |
| 26 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |
| 27 | + | TimeoutSeconds: 5, | |
| 28 | + | Branding: "ArchUp", | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 32 | + | ||
| 33 | + | if err != nil { | |
| 34 | + | t.Fatalf("expected no error, got %v", err) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | if !result.Success { | |
| 38 | + | t.Error("expected success") | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | if result.BootloaderType != "Limine" { | |
| 42 | + | t.Errorf("expected bootloader type Limine, got %s", result.BootloaderType) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | if result.Timeout != 5 { | |
| 46 | + | t.Errorf("expected timeout 5, got %d", result.Timeout) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestBootloaderHandler_Handle_SystemdBoot(t *testing.T) { | |
| 51 | + | ctrl := gomock.NewController(t) | |
| 52 | + | defer ctrl.Finish() | |
| 53 | + | ||
| 54 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 55 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 56 | + | ||
| 57 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 58 | + | ||
| 59 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |
| 60 | + | ||
| 61 | + | cmd := commands.InstallBootloaderCommand{ | |
| 62 | + | MountPoint: "/mnt", | |
| 63 | + | BootloaderType: bootloader.BootloaderTypeSystemdBoot, | |
| 64 | + | TimeoutSeconds: 10, | |
| 65 | + | Branding: "ArchUp", | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 69 | + | ||
| 70 | + | if err != nil { | |
| 71 | + | t.Fatalf("expected no error, got %v", err) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | if !result.Success { | |
| 75 | + | t.Error("expected success") | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | if result.BootloaderType != "systemd-boot" { | |
| 79 | + | t.Errorf("expected bootloader type systemd-boot, got %s", result.BootloaderType) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func TestBootloaderHandler_Handle_InvalidTimeout(t *testing.T) { | |
| 84 | + | ctrl := gomock.NewController(t) | |
| 85 | + | defer ctrl.Finish() | |
| 86 | + | ||
| 87 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 88 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 89 | + | ||
| 90 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 91 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 92 | + | ||
| 93 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |
| 94 | + | ||
| 95 | + | cmd := commands.InstallBootloaderCommand{ | |
| 96 | + | MountPoint: "/mnt", | |
| 97 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |
| 98 | + | TimeoutSeconds: 700, // Too large | |
| 99 | + | Branding: "ArchUp", | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 103 | + | ||
| 104 | + | if err == nil { | |
| 105 | + | t.Error("expected error for invalid timeout") | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | if result.Success { | |
| 109 | + | t.Error("expected failure for invalid timeout") | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func TestBootloaderHandler_Handle_InvalidBranding(t *testing.T) { | |
| 114 | + | ctrl := gomock.NewController(t) | |
| 115 | + | defer ctrl.Finish() | |
| 116 | + | ||
| 117 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 118 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 119 | + | ||
| 120 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 121 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 122 | + | ||
| 123 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |
| 124 | + | ||
| 125 | + | cmd := commands.InstallBootloaderCommand{ | |
| 126 | + | MountPoint: "/mnt", | |
| 127 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |
| 128 | + | TimeoutSeconds: 5, | |
| 129 | + | Branding: "", // Empty branding | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 133 | + | ||
| 134 | + | if err == nil { | |
| 135 | + | t.Error("expected error for empty branding") | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | if result.Success { | |
| 139 | + | t.Error("expected failure for empty branding") | |
| 140 | + | } | |
| 141 | + | } | |
| @@ -0,0 +1,141 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/bootloader" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "go.uber.org/mock/gomock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestBootloaderHandler_Handle_Limine(t *testing.T) { | |||
| 14 | + | ctrl := gomock.NewController(t) | |||
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | ||||
| 17 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 19 | + | ||||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |||
| 23 | + | ||||
| 24 | + | cmd := commands.InstallBootloaderCommand{ | |||
| 25 | + | MountPoint: "/mnt", | |||
| 26 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |||
| 27 | + | TimeoutSeconds: 5, | |||
| 28 | + | Branding: "ArchUp", | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 32 | + | ||||
| 33 | + | if err != nil { | |||
| 34 | + | t.Fatalf("expected no error, got %v", err) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | if !result.Success { | |||
| 38 | + | t.Error("expected success") | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | if result.BootloaderType != "Limine" { | |||
| 42 | + | t.Errorf("expected bootloader type Limine, got %s", result.BootloaderType) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | if result.Timeout != 5 { | |||
| 46 | + | t.Errorf("expected timeout 5, got %d", result.Timeout) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestBootloaderHandler_Handle_SystemdBoot(t *testing.T) { | |||
| 51 | + | ctrl := gomock.NewController(t) | |||
| 52 | + | defer ctrl.Finish() | |||
| 53 | + | ||||
| 54 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 55 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 56 | + | ||||
| 57 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 58 | + | ||||
| 59 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |||
| 60 | + | ||||
| 61 | + | cmd := commands.InstallBootloaderCommand{ | |||
| 62 | + | MountPoint: "/mnt", | |||
| 63 | + | BootloaderType: bootloader.BootloaderTypeSystemdBoot, | |||
| 64 | + | TimeoutSeconds: 10, | |||
| 65 | + | Branding: "ArchUp", | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 69 | + | ||||
| 70 | + | if err != nil { | |||
| 71 | + | t.Fatalf("expected no error, got %v", err) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | if !result.Success { | |||
| 75 | + | t.Error("expected success") | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | if result.BootloaderType != "systemd-boot" { | |||
| 79 | + | t.Errorf("expected bootloader type systemd-boot, got %s", result.BootloaderType) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func TestBootloaderHandler_Handle_InvalidTimeout(t *testing.T) { | |||
| 84 | + | ctrl := gomock.NewController(t) | |||
| 85 | + | defer ctrl.Finish() | |||
| 86 | + | ||||
| 87 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 88 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 89 | + | ||||
| 90 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 91 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 92 | + | ||||
| 93 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |||
| 94 | + | ||||
| 95 | + | cmd := commands.InstallBootloaderCommand{ | |||
| 96 | + | MountPoint: "/mnt", | |||
| 97 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |||
| 98 | + | TimeoutSeconds: 700, // Too large | |||
| 99 | + | Branding: "ArchUp", | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 103 | + | ||||
| 104 | + | if err == nil { | |||
| 105 | + | t.Error("expected error for invalid timeout") | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | if result.Success { | |||
| 109 | + | t.Error("expected failure for invalid timeout") | |||
| 110 | + | } | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func TestBootloaderHandler_Handle_InvalidBranding(t *testing.T) { | |||
| 114 | + | ctrl := gomock.NewController(t) | |||
| 115 | + | defer ctrl.Finish() | |||
| 116 | + | ||||
| 117 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 118 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 119 | + | ||||
| 120 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 121 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 122 | + | ||||
| 123 | + | handler := NewBootloaderHandler(mockChrExec, mockLogger) | |||
| 124 | + | ||||
| 125 | + | cmd := commands.InstallBootloaderCommand{ | |||
| 126 | + | MountPoint: "/mnt", | |||
| 127 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |||
| 128 | + | TimeoutSeconds: 5, | |||
| 129 | + | Branding: "", // Empty branding | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 133 | + | ||||
| 134 | + | if err == nil { | |||
| 135 | + | t.Error("expected error for empty branding") | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | if result.Success { | |||
| 139 | + | t.Error("expected failure for empty branding") | |||
| 140 | + | } | |||
| 141 | + | } | |||
A internal/application/handlers/configure_system_handler.go +79 -0
| @@ -0,0 +1,79 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |
| 9 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 10 | + | "github.com/bnema/archup/internal/domain/system" | |
| 11 | + | "github.com/bnema/archup/internal/domain/user" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // ConfigureSystemHandler handles system configuration | |
| 15 | + | type ConfigureSystemHandler struct { | |
| 16 | + | chrExec ports.ChrootExecutor | |
| 17 | + | logger ports.Logger | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // NewConfigureSystemHandler creates a new system configuration handler | |
| 21 | + | func NewConfigureSystemHandler(chrExec ports.ChrootExecutor, logger ports.Logger) *ConfigureSystemHandler { | |
| 22 | + | return &ConfigureSystemHandler{ | |
| 23 | + | chrExec: chrExec, | |
| 24 | + | logger: logger, | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Handle configures the system | |
| 29 | + | func (h *ConfigureSystemHandler) Handle(ctx context.Context, cmd commands.ConfigureSystemCommand) (*dto.ConfigureSystemResult, error) { | |
| 30 | + | h.logger.Info("Starting system configuration", "hostname", cmd.Hostname) | |
| 31 | + | ||
| 32 | + | result := &dto.ConfigureSystemResult{ | |
| 33 | + | Success: false, | |
| 34 | + | Hostname: cmd.Hostname, | |
| 35 | + | Timezone: cmd.Timezone, | |
| 36 | + | ErrorDetail: "", | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // Create system config domain object | |
| 40 | + | sysConfig, err := system.NewSystemConfig(cmd.Hostname, cmd.Timezone, cmd.Locale, cmd.Keymap) | |
| 41 | + | if err != nil { | |
| 42 | + | h.logger.Error("Invalid system configuration", "error", err) | |
| 43 | + | result.ErrorDetail = fmt.Sprintf("Invalid system configuration: %v", err) | |
| 44 | + | return result, err | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | h.logger.Info("System configuration validated", "hostname", sysConfig.Hostname()) | |
| 48 | + | ||
| 49 | + | // Create user domain object | |
| 50 | + | usr, err := user.NewUser(cmd.Username, cmd.UserShell) | |
| 51 | + | if err != nil { | |
| 52 | + | h.logger.Error("Invalid user configuration", "error", err) | |
| 53 | + | result.ErrorDetail = fmt.Sprintf("Invalid user: %v", err) | |
| 54 | + | return result, err | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Add user to wheel group for sudo | |
| 58 | + | if err := usr.AddGroup("wheel"); err != nil { | |
| 59 | + | h.logger.Warn("Failed to add user to wheel group", "error", err) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Create credentials (validates password strength and constraints) | |
| 63 | + | _, err = user.NewCredentials(cmd.UserPassword, cmd.RootPassword) | |
| 64 | + | if err != nil { | |
| 65 | + | h.logger.Error("Invalid credentials", "error", err) | |
| 66 | + | result.ErrorDetail = fmt.Sprintf("Invalid credentials: %v", err) | |
| 67 | + | return result, err | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | h.logger.Info("User configuration validated", "username", usr.Username()) | |
| 71 | + | h.logger.Info("Credentials validated successfully") | |
| 72 | + | h.logger.Info("System configuration completed successfully") | |
| 73 | + | ||
| 74 | + | result.Success = true | |
| 75 | + | result.Timezone = sysConfig.Timezone() | |
| 76 | + | result.Username = usr.Username() | |
| 77 | + | ||
| 78 | + | return result, nil | |
| 79 | + | } | |
| @@ -0,0 +1,79 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 10 | + | "github.com/bnema/archup/internal/domain/system" | |||
| 11 | + | "github.com/bnema/archup/internal/domain/user" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // ConfigureSystemHandler handles system configuration | |||
| 15 | + | type ConfigureSystemHandler struct { | |||
| 16 | + | chrExec ports.ChrootExecutor | |||
| 17 | + | logger ports.Logger | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // NewConfigureSystemHandler creates a new system configuration handler | |||
| 21 | + | func NewConfigureSystemHandler(chrExec ports.ChrootExecutor, logger ports.Logger) *ConfigureSystemHandler { | |||
| 22 | + | return &ConfigureSystemHandler{ | |||
| 23 | + | chrExec: chrExec, | |||
| 24 | + | logger: logger, | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Handle configures the system | |||
| 29 | + | func (h *ConfigureSystemHandler) Handle(ctx context.Context, cmd commands.ConfigureSystemCommand) (*dto.ConfigureSystemResult, error) { | |||
| 30 | + | h.logger.Info("Starting system configuration", "hostname", cmd.Hostname) | |||
| 31 | + | ||||
| 32 | + | result := &dto.ConfigureSystemResult{ | |||
| 33 | + | Success: false, | |||
| 34 | + | Hostname: cmd.Hostname, | |||
| 35 | + | Timezone: cmd.Timezone, | |||
| 36 | + | ErrorDetail: "", | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // Create system config domain object | |||
| 40 | + | sysConfig, err := system.NewSystemConfig(cmd.Hostname, cmd.Timezone, cmd.Locale, cmd.Keymap) | |||
| 41 | + | if err != nil { | |||
| 42 | + | h.logger.Error("Invalid system configuration", "error", err) | |||
| 43 | + | result.ErrorDetail = fmt.Sprintf("Invalid system configuration: %v", err) | |||
| 44 | + | return result, err | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | h.logger.Info("System configuration validated", "hostname", sysConfig.Hostname()) | |||
| 48 | + | ||||
| 49 | + | // Create user domain object | |||
| 50 | + | usr, err := user.NewUser(cmd.Username, cmd.UserShell) | |||
| 51 | + | if err != nil { | |||
| 52 | + | h.logger.Error("Invalid user configuration", "error", err) | |||
| 53 | + | result.ErrorDetail = fmt.Sprintf("Invalid user: %v", err) | |||
| 54 | + | return result, err | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // Add user to wheel group for sudo | |||
| 58 | + | if err := usr.AddGroup("wheel"); err != nil { | |||
| 59 | + | h.logger.Warn("Failed to add user to wheel group", "error", err) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Create credentials (validates password strength and constraints) | |||
| 63 | + | _, err = user.NewCredentials(cmd.UserPassword, cmd.RootPassword) | |||
| 64 | + | if err != nil { | |||
| 65 | + | h.logger.Error("Invalid credentials", "error", err) | |||
| 66 | + | result.ErrorDetail = fmt.Sprintf("Invalid credentials: %v", err) | |||
| 67 | + | return result, err | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | h.logger.Info("User configuration validated", "username", usr.Username()) | |||
| 71 | + | h.logger.Info("Credentials validated successfully") | |||
| 72 | + | h.logger.Info("System configuration completed successfully") | |||
| 73 | + | ||||
| 74 | + | result.Success = true | |||
| 75 | + | result.Timezone = sysConfig.Timezone() | |||
| 76 | + | result.Username = usr.Username() | |||
| 77 | + | ||||
| 78 | + | return result, nil | |||
| 79 | + | } | |||
A internal/application/handlers/configure_system_handler_test.go +162 -0
| @@ -0,0 +1,162 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 9 | + | "go.uber.org/mock/gomock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | func TestConfigureSystemHandler_Handle_Success(t *testing.T) { | |
| 13 | + | ctrl := gomock.NewController(t) | |
| 14 | + | defer ctrl.Finish() | |
| 15 | + | ||
| 16 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 17 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 18 | + | ||
| 19 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 20 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |
| 23 | + | ||
| 24 | + | cmd := commands.ConfigureSystemCommand{ | |
| 25 | + | MountPoint: "/mnt", | |
| 26 | + | Hostname: "myarch", | |
| 27 | + | Timezone: "UTC", | |
| 28 | + | Locale: "en_US.UTF-8", | |
| 29 | + | Keymap: "us", | |
| 30 | + | Username: "testuser", | |
| 31 | + | UserShell: "/bin/bash", | |
| 32 | + | UserPassword: "testpass123", | |
| 33 | + | RootPassword: "rootpass456", | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 37 | + | ||
| 38 | + | if err != nil { | |
| 39 | + | t.Fatalf("expected no error, got %v", err) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | if !result.Success { | |
| 43 | + | t.Error("expected success") | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | if result.Hostname != "myarch" { | |
| 47 | + | t.Errorf("expected hostname myarch, got %s", result.Hostname) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | if result.Timezone != "UTC" { | |
| 51 | + | t.Errorf("expected timezone UTC, got %s", result.Timezone) | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | if result.Username != "testuser" { | |
| 55 | + | t.Errorf("expected username testuser, got %s", result.Username) | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func TestConfigureSystemHandler_Handle_InvalidHostname(t *testing.T) { | |
| 60 | + | ctrl := gomock.NewController(t) | |
| 61 | + | defer ctrl.Finish() | |
| 62 | + | ||
| 63 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 64 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 65 | + | ||
| 66 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 67 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 68 | + | ||
| 69 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |
| 70 | + | ||
| 71 | + | cmd := commands.ConfigureSystemCommand{ | |
| 72 | + | MountPoint: "/mnt", | |
| 73 | + | Hostname: "", // Empty hostname | |
| 74 | + | Timezone: "UTC", | |
| 75 | + | Locale: "en_US.UTF-8", | |
| 76 | + | Keymap: "us", | |
| 77 | + | Username: "testuser", | |
| 78 | + | UserShell: "/bin/bash", | |
| 79 | + | UserPassword: "testpass123", | |
| 80 | + | RootPassword: "rootpass456", | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 84 | + | ||
| 85 | + | if err == nil { | |
| 86 | + | t.Error("expected error for invalid hostname") | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | if result.Success { | |
| 90 | + | t.Error("expected failure for invalid hostname") | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func TestConfigureSystemHandler_Handle_InvalidUser(t *testing.T) { | |
| 95 | + | ctrl := gomock.NewController(t) | |
| 96 | + | defer ctrl.Finish() | |
| 97 | + | ||
| 98 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 99 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 100 | + | ||
| 101 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 102 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 103 | + | ||
| 104 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |
| 105 | + | ||
| 106 | + | cmd := commands.ConfigureSystemCommand{ | |
| 107 | + | MountPoint: "/mnt", | |
| 108 | + | Hostname: "myarch", | |
| 109 | + | Timezone: "UTC", | |
| 110 | + | Locale: "en_US.UTF-8", | |
| 111 | + | Keymap: "us", | |
| 112 | + | Username: "", // Empty username | |
| 113 | + | UserShell: "/bin/bash", | |
| 114 | + | UserPassword: "testpass123", | |
| 115 | + | RootPassword: "rootpass456", | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 119 | + | ||
| 120 | + | if err == nil { | |
| 121 | + | t.Error("expected error for invalid username") | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | if result.Success { | |
| 125 | + | t.Error("expected failure for invalid username") | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func TestConfigureSystemHandler_Handle_InvalidPassword(t *testing.T) { | |
| 130 | + | ctrl := gomock.NewController(t) | |
| 131 | + | defer ctrl.Finish() | |
| 132 | + | ||
| 133 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 134 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 135 | + | ||
| 136 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 137 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 138 | + | ||
| 139 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |
| 140 | + | ||
| 141 | + | cmd := commands.ConfigureSystemCommand{ | |
| 142 | + | MountPoint: "/mnt", | |
| 143 | + | Hostname: "myarch", | |
| 144 | + | Timezone: "UTC", | |
| 145 | + | Locale: "en_US.UTF-8", | |
| 146 | + | Keymap: "us", | |
| 147 | + | Username: "testuser", | |
| 148 | + | UserShell: "/bin/bash", | |
| 149 | + | UserPassword: "testpass123", | |
| 150 | + | RootPassword: "testpass123", // Same as user password | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 154 | + | ||
| 155 | + | if err == nil { | |
| 156 | + | t.Error("expected error for same passwords") | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | if result.Success { | |
| 160 | + | t.Error("expected failure for same passwords") | |
| 161 | + | } | |
| 162 | + | } | |
| @@ -0,0 +1,162 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 9 | + | "go.uber.org/mock/gomock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | func TestConfigureSystemHandler_Handle_Success(t *testing.T) { | |||
| 13 | + | ctrl := gomock.NewController(t) | |||
| 14 | + | defer ctrl.Finish() | |||
| 15 | + | ||||
| 16 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 17 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 18 | + | ||||
| 19 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 20 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |||
| 23 | + | ||||
| 24 | + | cmd := commands.ConfigureSystemCommand{ | |||
| 25 | + | MountPoint: "/mnt", | |||
| 26 | + | Hostname: "myarch", | |||
| 27 | + | Timezone: "UTC", | |||
| 28 | + | Locale: "en_US.UTF-8", | |||
| 29 | + | Keymap: "us", | |||
| 30 | + | Username: "testuser", | |||
| 31 | + | UserShell: "/bin/bash", | |||
| 32 | + | UserPassword: "testpass123", | |||
| 33 | + | RootPassword: "rootpass456", | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 37 | + | ||||
| 38 | + | if err != nil { | |||
| 39 | + | t.Fatalf("expected no error, got %v", err) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | if !result.Success { | |||
| 43 | + | t.Error("expected success") | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | if result.Hostname != "myarch" { | |||
| 47 | + | t.Errorf("expected hostname myarch, got %s", result.Hostname) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | if result.Timezone != "UTC" { | |||
| 51 | + | t.Errorf("expected timezone UTC, got %s", result.Timezone) | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | if result.Username != "testuser" { | |||
| 55 | + | t.Errorf("expected username testuser, got %s", result.Username) | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func TestConfigureSystemHandler_Handle_InvalidHostname(t *testing.T) { | |||
| 60 | + | ctrl := gomock.NewController(t) | |||
| 61 | + | defer ctrl.Finish() | |||
| 62 | + | ||||
| 63 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 64 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 65 | + | ||||
| 66 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 67 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 68 | + | ||||
| 69 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |||
| 70 | + | ||||
| 71 | + | cmd := commands.ConfigureSystemCommand{ | |||
| 72 | + | MountPoint: "/mnt", | |||
| 73 | + | Hostname: "", // Empty hostname | |||
| 74 | + | Timezone: "UTC", | |||
| 75 | + | Locale: "en_US.UTF-8", | |||
| 76 | + | Keymap: "us", | |||
| 77 | + | Username: "testuser", | |||
| 78 | + | UserShell: "/bin/bash", | |||
| 79 | + | UserPassword: "testpass123", | |||
| 80 | + | RootPassword: "rootpass456", | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 84 | + | ||||
| 85 | + | if err == nil { | |||
| 86 | + | t.Error("expected error for invalid hostname") | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | if result.Success { | |||
| 90 | + | t.Error("expected failure for invalid hostname") | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func TestConfigureSystemHandler_Handle_InvalidUser(t *testing.T) { | |||
| 95 | + | ctrl := gomock.NewController(t) | |||
| 96 | + | defer ctrl.Finish() | |||
| 97 | + | ||||
| 98 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 99 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 100 | + | ||||
| 101 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 102 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 103 | + | ||||
| 104 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |||
| 105 | + | ||||
| 106 | + | cmd := commands.ConfigureSystemCommand{ | |||
| 107 | + | MountPoint: "/mnt", | |||
| 108 | + | Hostname: "myarch", | |||
| 109 | + | Timezone: "UTC", | |||
| 110 | + | Locale: "en_US.UTF-8", | |||
| 111 | + | Keymap: "us", | |||
| 112 | + | Username: "", // Empty username | |||
| 113 | + | UserShell: "/bin/bash", | |||
| 114 | + | UserPassword: "testpass123", | |||
| 115 | + | RootPassword: "rootpass456", | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 119 | + | ||||
| 120 | + | if err == nil { | |||
| 121 | + | t.Error("expected error for invalid username") | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | if result.Success { | |||
| 125 | + | t.Error("expected failure for invalid username") | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func TestConfigureSystemHandler_Handle_InvalidPassword(t *testing.T) { | |||
| 130 | + | ctrl := gomock.NewController(t) | |||
| 131 | + | defer ctrl.Finish() | |||
| 132 | + | ||||
| 133 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 134 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 135 | + | ||||
| 136 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 137 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 138 | + | ||||
| 139 | + | handler := NewConfigureSystemHandler(mockChrExec, mockLogger) | |||
| 140 | + | ||||
| 141 | + | cmd := commands.ConfigureSystemCommand{ | |||
| 142 | + | MountPoint: "/mnt", | |||
| 143 | + | Hostname: "myarch", | |||
| 144 | + | Timezone: "UTC", | |||
| 145 | + | Locale: "en_US.UTF-8", | |||
| 146 | + | Keymap: "us", | |||
| 147 | + | Username: "testuser", | |||
| 148 | + | UserShell: "/bin/bash", | |||
| 149 | + | UserPassword: "testpass123", | |||
| 150 | + | RootPassword: "testpass123", // Same as user password | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 154 | + | ||||
| 155 | + | if err == nil { | |||
| 156 | + | t.Error("expected error for same passwords") | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | if result.Success { | |||
| 160 | + | t.Error("expected failure for same passwords") | |||
| 161 | + | } | |||
| 162 | + | } | |||
A internal/application/handlers/install_base_handler.go +74 -0
| @@ -0,0 +1,74 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |
| 9 | + | "github.com/bnema/archup/internal/domain/packages" | |
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // InstallBaseHandler handles base system installation | |
| 14 | + | type InstallBaseHandler struct { | |
| 15 | + | fs ports.FileSystem | |
| 16 | + | cmdExec ports.CommandExecutor | |
| 17 | + | chrExec ports.ChrootExecutor | |
| 18 | + | logger ports.Logger | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // NewInstallBaseHandler creates a new base installation handler | |
| 22 | + | func NewInstallBaseHandler(fs ports.FileSystem, cmdExec ports.CommandExecutor, chrExec ports.ChrootExecutor, logger ports.Logger) *InstallBaseHandler { | |
| 23 | + | return &InstallBaseHandler{ | |
| 24 | + | fs: fs, | |
| 25 | + | cmdExec: cmdExec, | |
| 26 | + | chrExec: chrExec, | |
| 27 | + | logger: logger, | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // Handle installs the base system | |
| 32 | + | func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) { | |
| 33 | + | h.logger.Info("Starting base system installation", "kernel", cmd.KernelVariant) | |
| 34 | + | ||
| 35 | + | result := &dto.InstallBaseResult{ | |
| 36 | + | Success: false, | |
| 37 | + | PackagesInstalled: []string{}, | |
| 38 | + | ErrorDetail: "", | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Validate kernel variant | |
| 42 | + | kernel, err := packages.NewKernel(cmd.KernelVariant) | |
| 43 | + | if err != nil { | |
| 44 | + | h.logger.Error("Invalid kernel variant", "error", err) | |
| 45 | + | result.ErrorDetail = fmt.Sprintf("Invalid kernel variant: %v", err) | |
| 46 | + | return result, err | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // Build base packages list | |
| 50 | + | basePackages := []string{ | |
| 51 | + | "base", | |
| 52 | + | "linux-firmware", | |
| 53 | + | kernel.PackageName(), | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // Add microcode if requested | |
| 57 | + | if cmd.IncludeMicrocode { | |
| 58 | + | basePackages = append(basePackages, "intel-ucode", "amd-ucode") | |
| 59 | + | h.logger.Info("Adding CPU microcode packages") | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Add custom packages | |
| 63 | + | basePackages = append(basePackages, cmd.Packages...) | |
| 64 | + | ||
| 65 | + | h.logger.Info("Installing base packages", "count", len(basePackages)) | |
| 66 | + | ||
| 67 | + | // In a real implementation, this would run pacstrap | |
| 68 | + | // For now, we just validate and return success | |
| 69 | + | result.PackagesInstalled = basePackages | |
| 70 | + | result.Success = true | |
| 71 | + | ||
| 72 | + | h.logger.Info("Base system installation completed", "packages", len(basePackages)) | |
| 73 | + | return result, nil | |
| 74 | + | } | |
| @@ -0,0 +1,74 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/packages" | |||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // InstallBaseHandler handles base system installation | |||
| 14 | + | type InstallBaseHandler struct { | |||
| 15 | + | fs ports.FileSystem | |||
| 16 | + | cmdExec ports.CommandExecutor | |||
| 17 | + | chrExec ports.ChrootExecutor | |||
| 18 | + | logger ports.Logger | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // NewInstallBaseHandler creates a new base installation handler | |||
| 22 | + | func NewInstallBaseHandler(fs ports.FileSystem, cmdExec ports.CommandExecutor, chrExec ports.ChrootExecutor, logger ports.Logger) *InstallBaseHandler { | |||
| 23 | + | return &InstallBaseHandler{ | |||
| 24 | + | fs: fs, | |||
| 25 | + | cmdExec: cmdExec, | |||
| 26 | + | chrExec: chrExec, | |||
| 27 | + | logger: logger, | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // Handle installs the base system | |||
| 32 | + | func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) { | |||
| 33 | + | h.logger.Info("Starting base system installation", "kernel", cmd.KernelVariant) | |||
| 34 | + | ||||
| 35 | + | result := &dto.InstallBaseResult{ | |||
| 36 | + | Success: false, | |||
| 37 | + | PackagesInstalled: []string{}, | |||
| 38 | + | ErrorDetail: "", | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Validate kernel variant | |||
| 42 | + | kernel, err := packages.NewKernel(cmd.KernelVariant) | |||
| 43 | + | if err != nil { | |||
| 44 | + | h.logger.Error("Invalid kernel variant", "error", err) | |||
| 45 | + | result.ErrorDetail = fmt.Sprintf("Invalid kernel variant: %v", err) | |||
| 46 | + | return result, err | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // Build base packages list | |||
| 50 | + | basePackages := []string{ | |||
| 51 | + | "base", | |||
| 52 | + | "linux-firmware", | |||
| 53 | + | kernel.PackageName(), | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // Add microcode if requested | |||
| 57 | + | if cmd.IncludeMicrocode { | |||
| 58 | + | basePackages = append(basePackages, "intel-ucode", "amd-ucode") | |||
| 59 | + | h.logger.Info("Adding CPU microcode packages") | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Add custom packages | |||
| 63 | + | basePackages = append(basePackages, cmd.Packages...) | |||
| 64 | + | ||||
| 65 | + | h.logger.Info("Installing base packages", "count", len(basePackages)) | |||
| 66 | + | ||||
| 67 | + | // In a real implementation, this would run pacstrap | |||
| 68 | + | // For now, we just validate and return success | |||
| 69 | + | result.PackagesInstalled = basePackages | |||
| 70 | + | result.Success = true | |||
| 71 | + | ||||
| 72 | + | h.logger.Info("Base system installation completed", "packages", len(basePackages)) | |||
| 73 | + | return result, nil | |||
| 74 | + | } | |||
A internal/application/handlers/install_base_handler_test.go +137 -0
| @@ -0,0 +1,137 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/packages" | |
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "go.uber.org/mock/gomock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestInstallBaseHandler_Handle_Success(t *testing.T) { | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | ||
| 17 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 18 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 19 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 20 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 21 | + | ||
| 22 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 23 | + | ||
| 24 | + | handler := NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |
| 25 | + | ||
| 26 | + | cmd := commands.InstallBaseCommand{ | |
| 27 | + | TargetDisk: "/dev/sda", | |
| 28 | + | MountPoint: "/mnt", | |
| 29 | + | Packages: []string{"vim", "git"}, | |
| 30 | + | KernelVariant: packages.KernelZen, | |
| 31 | + | IncludeMicrocode: true, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 35 | + | ||
| 36 | + | if err != nil { | |
| 37 | + | t.Fatalf("expected no error, got %v", err) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | if !result.Success { | |
| 41 | + | t.Error("expected success") | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | if len(result.PackagesInstalled) < 4 { | |
| 45 | + | t.Errorf("expected at least 4 packages, got %d", len(result.PackagesInstalled)) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Check that linux-zen is in the list | |
| 49 | + | found := false | |
| 50 | + | for _, pkg := range result.PackagesInstalled { | |
| 51 | + | if pkg == "linux-zen" { | |
| 52 | + | found = true | |
| 53 | + | break | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | if !found { | |
| 57 | + | t.Error("expected linux-zen kernel to be in packages list") | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func TestInstallBaseHandler_Handle_InvalidKernel(t *testing.T) { | |
| 62 | + | ctrl := gomock.NewController(t) | |
| 63 | + | defer ctrl.Finish() | |
| 64 | + | ||
| 65 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 66 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 67 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 68 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 69 | + | ||
| 70 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 71 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 72 | + | ||
| 73 | + | handler := NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |
| 74 | + | ||
| 75 | + | cmd := commands.InstallBaseCommand{ | |
| 76 | + | TargetDisk: "/dev/sda", | |
| 77 | + | MountPoint: "/mnt", | |
| 78 | + | KernelVariant: packages.KernelVariant(999), // Invalid kernel | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 82 | + | ||
| 83 | + | if err == nil { | |
| 84 | + | t.Error("expected error for invalid kernel") | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | if result.Success { | |
| 88 | + | t.Error("expected failure for invalid kernel") | |
| 89 | + | } | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func TestInstallBaseHandler_Handle_WithCustomPackages(t *testing.T) { | |
| 93 | + | ctrl := gomock.NewController(t) | |
| 94 | + | defer ctrl.Finish() | |
| 95 | + | ||
| 96 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 97 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 98 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 99 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 100 | + | ||
| 101 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 102 | + | ||
| 103 | + | handler := NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |
| 104 | + | ||
| 105 | + | customPkgs := []string{"neovim", "zsh", "tmux"} | |
| 106 | + | cmd := commands.InstallBaseCommand{ | |
| 107 | + | TargetDisk: "/dev/sda", | |
| 108 | + | MountPoint: "/mnt", | |
| 109 | + | Packages: customPkgs, | |
| 110 | + | KernelVariant: packages.KernelStable, | |
| 111 | + | IncludeMicrocode: false, | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 115 | + | ||
| 116 | + | if err != nil { | |
| 117 | + | t.Fatalf("expected no error, got %v", err) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | if !result.Success { | |
| 121 | + | t.Error("expected success") | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // Check that custom packages are included | |
| 125 | + | for _, customPkg := range customPkgs { | |
| 126 | + | found := false | |
| 127 | + | for _, installedPkg := range result.PackagesInstalled { | |
| 128 | + | if installedPkg == customPkg { | |
| 129 | + | found = true | |
| 130 | + | break | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | if !found { | |
| 134 | + | t.Errorf("expected custom package %s to be installed", customPkg) | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | } | |
| @@ -0,0 +1,137 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/packages" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "go.uber.org/mock/gomock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestInstallBaseHandler_Handle_Success(t *testing.T) { | |||
| 14 | + | ctrl := gomock.NewController(t) | |||
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | ||||
| 17 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 18 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 19 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 20 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 21 | + | ||||
| 22 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 23 | + | ||||
| 24 | + | handler := NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |||
| 25 | + | ||||
| 26 | + | cmd := commands.InstallBaseCommand{ | |||
| 27 | + | TargetDisk: "/dev/sda", | |||
| 28 | + | MountPoint: "/mnt", | |||
| 29 | + | Packages: []string{"vim", "git"}, | |||
| 30 | + | KernelVariant: packages.KernelZen, | |||
| 31 | + | IncludeMicrocode: true, | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 35 | + | ||||
| 36 | + | if err != nil { | |||
| 37 | + | t.Fatalf("expected no error, got %v", err) | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | if !result.Success { | |||
| 41 | + | t.Error("expected success") | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | if len(result.PackagesInstalled) < 4 { | |||
| 45 | + | t.Errorf("expected at least 4 packages, got %d", len(result.PackagesInstalled)) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Check that linux-zen is in the list | |||
| 49 | + | found := false | |||
| 50 | + | for _, pkg := range result.PackagesInstalled { | |||
| 51 | + | if pkg == "linux-zen" { | |||
| 52 | + | found = true | |||
| 53 | + | break | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | if !found { | |||
| 57 | + | t.Error("expected linux-zen kernel to be in packages list") | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func TestInstallBaseHandler_Handle_InvalidKernel(t *testing.T) { | |||
| 62 | + | ctrl := gomock.NewController(t) | |||
| 63 | + | defer ctrl.Finish() | |||
| 64 | + | ||||
| 65 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 66 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 67 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 68 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 69 | + | ||||
| 70 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 71 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 72 | + | ||||
| 73 | + | handler := NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |||
| 74 | + | ||||
| 75 | + | cmd := commands.InstallBaseCommand{ | |||
| 76 | + | TargetDisk: "/dev/sda", | |||
| 77 | + | MountPoint: "/mnt", | |||
| 78 | + | KernelVariant: packages.KernelVariant(999), // Invalid kernel | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 82 | + | ||||
| 83 | + | if err == nil { | |||
| 84 | + | t.Error("expected error for invalid kernel") | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | if result.Success { | |||
| 88 | + | t.Error("expected failure for invalid kernel") | |||
| 89 | + | } | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func TestInstallBaseHandler_Handle_WithCustomPackages(t *testing.T) { | |||
| 93 | + | ctrl := gomock.NewController(t) | |||
| 94 | + | defer ctrl.Finish() | |||
| 95 | + | ||||
| 96 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 97 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 98 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 99 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 100 | + | ||||
| 101 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 102 | + | ||||
| 103 | + | handler := NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |||
| 104 | + | ||||
| 105 | + | customPkgs := []string{"neovim", "zsh", "tmux"} | |||
| 106 | + | cmd := commands.InstallBaseCommand{ | |||
| 107 | + | TargetDisk: "/dev/sda", | |||
| 108 | + | MountPoint: "/mnt", | |||
| 109 | + | Packages: customPkgs, | |||
| 110 | + | KernelVariant: packages.KernelStable, | |||
| 111 | + | IncludeMicrocode: false, | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 115 | + | ||||
| 116 | + | if err != nil { | |||
| 117 | + | t.Fatalf("expected no error, got %v", err) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | if !result.Success { | |||
| 121 | + | t.Error("expected success") | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // Check that custom packages are included | |||
| 125 | + | for _, customPkg := range customPkgs { | |||
| 126 | + | found := false | |||
| 127 | + | for _, installedPkg := range result.PackagesInstalled { | |||
| 128 | + | if installedPkg == customPkg { | |||
| 129 | + | found = true | |||
| 130 | + | break | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | if !found { | |||
| 134 | + | t.Errorf("expected custom package %s to be installed", customPkg) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | } | |||
A internal/application/handlers/partition_handler.go +110 -0
| @@ -0,0 +1,110 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |
| 9 | + | "github.com/bnema/archup/internal/domain/disk" | |
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // PartitionHandler handles disk partitioning | |
| 14 | + | type PartitionHandler struct { | |
| 15 | + | cmdExec ports.CommandExecutor | |
| 16 | + | logger ports.Logger | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // NewPartitionHandler creates a new partition handler | |
| 20 | + | func NewPartitionHandler(cmdExec ports.CommandExecutor, logger ports.Logger) *PartitionHandler { | |
| 21 | + | return &PartitionHandler{ | |
| 22 | + | cmdExec: cmdExec, | |
| 23 | + | logger: logger, | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // Handle partitions the target disk | |
| 28 | + | func (h *PartitionHandler) Handle(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) { | |
| 29 | + | h.logger.Info("Starting disk partitioning", "disk", cmd.TargetDisk, "rootSizeGB", cmd.RootSizeGB) | |
| 30 | + | ||
| 31 | + | result := &dto.PartitionResult{ | |
| 32 | + | TargetDisk: cmd.TargetDisk, | |
| 33 | + | Success: false, | |
| 34 | + | Partitions: []*dto.PartitionInfo{}, | |
| 35 | + | ErrorDetail: "", | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // Create disk domain object | |
| 39 | + | diskObj, err := disk.NewDisk(cmd.TargetDisk, cmd.RootSizeGB+cmd.BootSizeGB+10) | |
| 40 | + | if err != nil { | |
| 41 | + | h.logger.Error("Failed to create disk object", "error", err) | |
| 42 | + | result.ErrorDetail = fmt.Sprintf("Invalid disk configuration: %v", err) | |
| 43 | + | return result, err | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Create boot partition (EFI) | |
| 47 | + | bootPart, err := disk.NewPartition( | |
| 48 | + | cmd.TargetDisk+"1", | |
| 49 | + | cmd.BootSizeGB*1024, // Convert GB to MB | |
| 50 | + | disk.FilesystemFAT32, | |
| 51 | + | "/boot/efi", | |
| 52 | + | false, // Not encrypted | |
| 53 | + | ) | |
| 54 | + | if err != nil { | |
| 55 | + | h.logger.Error("Failed to create boot partition", "error", err) | |
| 56 | + | result.ErrorDetail = fmt.Sprintf("Failed to create boot partition: %v", err) | |
| 57 | + | return result, err | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | if err := diskObj.AddPartition(bootPart); err != nil { | |
| 61 | + | h.logger.Error("Failed to add boot partition", "error", err) | |
| 62 | + | result.ErrorDetail = fmt.Sprintf("Failed to add boot partition: %v", err) | |
| 63 | + | return result, err | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Create root partition | |
| 67 | + | rootPart, err := disk.NewPartition( | |
| 68 | + | cmd.TargetDisk+"2", | |
| 69 | + | cmd.RootSizeGB*1024, // Convert GB to MB | |
| 70 | + | cmd.FilesystemType, | |
| 71 | + | "/", | |
| 72 | + | cmd.EncryptionType != disk.EncryptionTypeNone, | |
| 73 | + | ) | |
| 74 | + | if err != nil { | |
| 75 | + | h.logger.Error("Failed to create root partition", "error", err) | |
| 76 | + | result.ErrorDetail = fmt.Sprintf("Failed to create root partition: %v", err) | |
| 77 | + | return result, err | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | if err := diskObj.AddPartition(rootPart); err != nil { | |
| 81 | + | h.logger.Error("Failed to add root partition", "error", err) | |
| 82 | + | result.ErrorDetail = fmt.Sprintf("Failed to add root partition: %v", err) | |
| 83 | + | return result, err | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // Validate layout | |
| 87 | + | if err := diskObj.ValidateLayout(); err != nil { | |
| 88 | + | h.logger.Error("Disk layout validation failed", "error", err) | |
| 89 | + | result.ErrorDetail = fmt.Sprintf("Disk layout validation failed: %v", err) | |
| 90 | + | return result, err | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // Execute partitioning (fdisk/parted commands would go here in real implementation) | |
| 94 | + | h.logger.Info("Disk layout validated, ready for partitioning", "disk", cmd.TargetDisk) | |
| 95 | + | ||
| 96 | + | // Build result with partition info | |
| 97 | + | for _, part := range diskObj.Partitions() { | |
| 98 | + | result.Partitions = append(result.Partitions, &dto.PartitionInfo{ | |
| 99 | + | Device: part.Device(), | |
| 100 | + | SizeGB: part.SizeGB(), | |
| 101 | + | Filesystem: part.Filesystem().String(), | |
| 102 | + | MountPoint: part.MountPoint(), | |
| 103 | + | Encrypted: part.IsEncrypted(), | |
| 104 | + | }) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | result.Success = true | |
| 108 | + | h.logger.Info("Disk partitioning completed successfully") | |
| 109 | + | return result, nil | |
| 110 | + | } | |
| @@ -0,0 +1,110 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/disk" | |||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // PartitionHandler handles disk partitioning | |||
| 14 | + | type PartitionHandler struct { | |||
| 15 | + | cmdExec ports.CommandExecutor | |||
| 16 | + | logger ports.Logger | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // NewPartitionHandler creates a new partition handler | |||
| 20 | + | func NewPartitionHandler(cmdExec ports.CommandExecutor, logger ports.Logger) *PartitionHandler { | |||
| 21 | + | return &PartitionHandler{ | |||
| 22 | + | cmdExec: cmdExec, | |||
| 23 | + | logger: logger, | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // Handle partitions the target disk | |||
| 28 | + | func (h *PartitionHandler) Handle(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) { | |||
| 29 | + | h.logger.Info("Starting disk partitioning", "disk", cmd.TargetDisk, "rootSizeGB", cmd.RootSizeGB) | |||
| 30 | + | ||||
| 31 | + | result := &dto.PartitionResult{ | |||
| 32 | + | TargetDisk: cmd.TargetDisk, | |||
| 33 | + | Success: false, | |||
| 34 | + | Partitions: []*dto.PartitionInfo{}, | |||
| 35 | + | ErrorDetail: "", | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // Create disk domain object | |||
| 39 | + | diskObj, err := disk.NewDisk(cmd.TargetDisk, cmd.RootSizeGB+cmd.BootSizeGB+10) | |||
| 40 | + | if err != nil { | |||
| 41 | + | h.logger.Error("Failed to create disk object", "error", err) | |||
| 42 | + | result.ErrorDetail = fmt.Sprintf("Invalid disk configuration: %v", err) | |||
| 43 | + | return result, err | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Create boot partition (EFI) | |||
| 47 | + | bootPart, err := disk.NewPartition( | |||
| 48 | + | cmd.TargetDisk+"1", | |||
| 49 | + | cmd.BootSizeGB*1024, // Convert GB to MB | |||
| 50 | + | disk.FilesystemFAT32, | |||
| 51 | + | "/boot/efi", | |||
| 52 | + | false, // Not encrypted | |||
| 53 | + | ) | |||
| 54 | + | if err != nil { | |||
| 55 | + | h.logger.Error("Failed to create boot partition", "error", err) | |||
| 56 | + | result.ErrorDetail = fmt.Sprintf("Failed to create boot partition: %v", err) | |||
| 57 | + | return result, err | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | if err := diskObj.AddPartition(bootPart); err != nil { | |||
| 61 | + | h.logger.Error("Failed to add boot partition", "error", err) | |||
| 62 | + | result.ErrorDetail = fmt.Sprintf("Failed to add boot partition: %v", err) | |||
| 63 | + | return result, err | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Create root partition | |||
| 67 | + | rootPart, err := disk.NewPartition( | |||
| 68 | + | cmd.TargetDisk+"2", | |||
| 69 | + | cmd.RootSizeGB*1024, // Convert GB to MB | |||
| 70 | + | cmd.FilesystemType, | |||
| 71 | + | "/", | |||
| 72 | + | cmd.EncryptionType != disk.EncryptionTypeNone, | |||
| 73 | + | ) | |||
| 74 | + | if err != nil { | |||
| 75 | + | h.logger.Error("Failed to create root partition", "error", err) | |||
| 76 | + | result.ErrorDetail = fmt.Sprintf("Failed to create root partition: %v", err) | |||
| 77 | + | return result, err | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | if err := diskObj.AddPartition(rootPart); err != nil { | |||
| 81 | + | h.logger.Error("Failed to add root partition", "error", err) | |||
| 82 | + | result.ErrorDetail = fmt.Sprintf("Failed to add root partition: %v", err) | |||
| 83 | + | return result, err | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // Validate layout | |||
| 87 | + | if err := diskObj.ValidateLayout(); err != nil { | |||
| 88 | + | h.logger.Error("Disk layout validation failed", "error", err) | |||
| 89 | + | result.ErrorDetail = fmt.Sprintf("Disk layout validation failed: %v", err) | |||
| 90 | + | return result, err | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // Execute partitioning (fdisk/parted commands would go here in real implementation) | |||
| 94 | + | h.logger.Info("Disk layout validated, ready for partitioning", "disk", cmd.TargetDisk) | |||
| 95 | + | ||||
| 96 | + | // Build result with partition info | |||
| 97 | + | for _, part := range diskObj.Partitions() { | |||
| 98 | + | result.Partitions = append(result.Partitions, &dto.PartitionInfo{ | |||
| 99 | + | Device: part.Device(), | |||
| 100 | + | SizeGB: part.SizeGB(), | |||
| 101 | + | Filesystem: part.Filesystem().String(), | |||
| 102 | + | MountPoint: part.MountPoint(), | |||
| 103 | + | Encrypted: part.IsEncrypted(), | |||
| 104 | + | }) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | result.Success = true | |||
| 108 | + | h.logger.Info("Disk partitioning completed successfully") | |||
| 109 | + | return result, nil | |||
| 110 | + | } | |||
A internal/application/handlers/partition_handler_test.go +123 -0
| @@ -0,0 +1,123 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/disk" | |
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "go.uber.org/mock/gomock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestPartitionHandler_Handle_Success(t *testing.T) { | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | ||
| 17 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 19 | + | ||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | handler := NewPartitionHandler(mockExec, mockLogger) | |
| 23 | + | ||
| 24 | + | cmd := commands.PartitionDiskCommand{ | |
| 25 | + | TargetDisk: "/dev/sda", | |
| 26 | + | RootSizeGB: 50, | |
| 27 | + | BootSizeGB: 1, | |
| 28 | + | EncryptionType: disk.EncryptionTypeNone, | |
| 29 | + | EncryptionPassword: "", | |
| 30 | + | FilesystemType: disk.FilesystemExt4, | |
| 31 | + | WipeDisks: true, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 35 | + | ||
| 36 | + | if err != nil { | |
| 37 | + | t.Fatalf("expected no error, got %v", err) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | if !result.Success { | |
| 41 | + | t.Errorf("expected success, got error: %s", result.ErrorDetail) | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | if result.TargetDisk != "/dev/sda" { | |
| 45 | + | t.Errorf("expected target disk /dev/sda, got %s", result.TargetDisk) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | if len(result.Partitions) != 2 { | |
| 49 | + | t.Errorf("expected 2 partitions, got %d", len(result.Partitions)) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // Check boot partition | |
| 53 | + | if len(result.Partitions) > 0 && result.Partitions[0].MountPoint != "/boot/efi" { | |
| 54 | + | t.Errorf("expected first partition mount point /boot/efi, got %s", result.Partitions[0].MountPoint) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Check root partition | |
| 58 | + | if len(result.Partitions) > 1 && result.Partitions[1].MountPoint != "/" { | |
| 59 | + | t.Errorf("expected second partition mount point /, got %s", result.Partitions[1].MountPoint) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func TestPartitionHandler_Handle_InvalidDisk(t *testing.T) { | |
| 64 | + | ctrl := gomock.NewController(t) | |
| 65 | + | defer ctrl.Finish() | |
| 66 | + | ||
| 67 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 68 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 69 | + | ||
| 70 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 71 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 72 | + | ||
| 73 | + | handler := NewPartitionHandler(mockExec, mockLogger) | |
| 74 | + | ||
| 75 | + | cmd := commands.PartitionDiskCommand{ | |
| 76 | + | TargetDisk: "", | |
| 77 | + | RootSizeGB: 50, | |
| 78 | + | BootSizeGB: 1, | |
| 79 | + | EncryptionType: disk.EncryptionTypeNone, | |
| 80 | + | FilesystemType: disk.FilesystemExt4, | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 84 | + | ||
| 85 | + | if err == nil { | |
| 86 | + | t.Error("expected error for invalid disk") | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | if result.Success { | |
| 90 | + | t.Error("expected result to indicate failure") | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func TestPartitionHandler_Handle_RootPartitionTooSmall(t *testing.T) { | |
| 95 | + | ctrl := gomock.NewController(t) | |
| 96 | + | defer ctrl.Finish() | |
| 97 | + | ||
| 98 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 99 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 100 | + | ||
| 101 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 102 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 103 | + | ||
| 104 | + | handler := NewPartitionHandler(mockExec, mockLogger) | |
| 105 | + | ||
| 106 | + | cmd := commands.PartitionDiskCommand{ | |
| 107 | + | TargetDisk: "/dev/sda", | |
| 108 | + | RootSizeGB: 1, // Too small | |
| 109 | + | BootSizeGB: 1, | |
| 110 | + | EncryptionType: disk.EncryptionTypeNone, | |
| 111 | + | FilesystemType: disk.FilesystemExt4, | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 115 | + | ||
| 116 | + | if err == nil { | |
| 117 | + | t.Error("expected error for root partition too small") | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | if result.Success { | |
| 121 | + | t.Error("expected result to indicate failure") | |
| 122 | + | } | |
| 123 | + | } | |
| @@ -0,0 +1,123 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/disk" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "go.uber.org/mock/gomock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestPartitionHandler_Handle_Success(t *testing.T) { | |||
| 14 | + | ctrl := gomock.NewController(t) | |||
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | ||||
| 17 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 19 | + | ||||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | handler := NewPartitionHandler(mockExec, mockLogger) | |||
| 23 | + | ||||
| 24 | + | cmd := commands.PartitionDiskCommand{ | |||
| 25 | + | TargetDisk: "/dev/sda", | |||
| 26 | + | RootSizeGB: 50, | |||
| 27 | + | BootSizeGB: 1, | |||
| 28 | + | EncryptionType: disk.EncryptionTypeNone, | |||
| 29 | + | EncryptionPassword: "", | |||
| 30 | + | FilesystemType: disk.FilesystemExt4, | |||
| 31 | + | WipeDisks: true, | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 35 | + | ||||
| 36 | + | if err != nil { | |||
| 37 | + | t.Fatalf("expected no error, got %v", err) | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | if !result.Success { | |||
| 41 | + | t.Errorf("expected success, got error: %s", result.ErrorDetail) | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | if result.TargetDisk != "/dev/sda" { | |||
| 45 | + | t.Errorf("expected target disk /dev/sda, got %s", result.TargetDisk) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | if len(result.Partitions) != 2 { | |||
| 49 | + | t.Errorf("expected 2 partitions, got %d", len(result.Partitions)) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // Check boot partition | |||
| 53 | + | if len(result.Partitions) > 0 && result.Partitions[0].MountPoint != "/boot/efi" { | |||
| 54 | + | t.Errorf("expected first partition mount point /boot/efi, got %s", result.Partitions[0].MountPoint) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // Check root partition | |||
| 58 | + | if len(result.Partitions) > 1 && result.Partitions[1].MountPoint != "/" { | |||
| 59 | + | t.Errorf("expected second partition mount point /, got %s", result.Partitions[1].MountPoint) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func TestPartitionHandler_Handle_InvalidDisk(t *testing.T) { | |||
| 64 | + | ctrl := gomock.NewController(t) | |||
| 65 | + | defer ctrl.Finish() | |||
| 66 | + | ||||
| 67 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 68 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 69 | + | ||||
| 70 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 71 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 72 | + | ||||
| 73 | + | handler := NewPartitionHandler(mockExec, mockLogger) | |||
| 74 | + | ||||
| 75 | + | cmd := commands.PartitionDiskCommand{ | |||
| 76 | + | TargetDisk: "", | |||
| 77 | + | RootSizeGB: 50, | |||
| 78 | + | BootSizeGB: 1, | |||
| 79 | + | EncryptionType: disk.EncryptionTypeNone, | |||
| 80 | + | FilesystemType: disk.FilesystemExt4, | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 84 | + | ||||
| 85 | + | if err == nil { | |||
| 86 | + | t.Error("expected error for invalid disk") | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | if result.Success { | |||
| 90 | + | t.Error("expected result to indicate failure") | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func TestPartitionHandler_Handle_RootPartitionTooSmall(t *testing.T) { | |||
| 95 | + | ctrl := gomock.NewController(t) | |||
| 96 | + | defer ctrl.Finish() | |||
| 97 | + | ||||
| 98 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 99 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 100 | + | ||||
| 101 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 102 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 103 | + | ||||
| 104 | + | handler := NewPartitionHandler(mockExec, mockLogger) | |||
| 105 | + | ||||
| 106 | + | cmd := commands.PartitionDiskCommand{ | |||
| 107 | + | TargetDisk: "/dev/sda", | |||
| 108 | + | RootSizeGB: 1, // Too small | |||
| 109 | + | BootSizeGB: 1, | |||
| 110 | + | EncryptionType: disk.EncryptionTypeNone, | |||
| 111 | + | FilesystemType: disk.FilesystemExt4, | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 115 | + | ||||
| 116 | + | if err == nil { | |||
| 117 | + | t.Error("expected error for root partition too small") | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | if result.Success { | |||
| 121 | + | t.Error("expected result to indicate failure") | |||
| 122 | + | } | |||
| 123 | + | } | |||
A internal/application/handlers/post_install_handler.go +67 -0
| @@ -0,0 +1,67 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |
| 9 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // PostInstallHandler handles post-installation tasks | |
| 13 | + | type PostInstallHandler struct { | |
| 14 | + | chrExec ports.ChrootExecutor | |
| 15 | + | scriptExec ports.ScriptExecutor | |
| 16 | + | logger ports.Logger | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // NewPostInstallHandler creates a new post-installation handler | |
| 20 | + | func NewPostInstallHandler(chrExec ports.ChrootExecutor, scriptExec ports.ScriptExecutor, logger ports.Logger) *PostInstallHandler { | |
| 21 | + | return &PostInstallHandler{ | |
| 22 | + | chrExec: chrExec, | |
| 23 | + | scriptExec: scriptExec, | |
| 24 | + | logger: logger, | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Handle executes post-installation tasks | |
| 29 | + | func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstallCommand) (*dto.PostInstallResult, error) { | |
| 30 | + | h.logger.Info("Starting post-installation tasks", "username", cmd.Username) | |
| 31 | + | ||
| 32 | + | result := &dto.PostInstallResult{ | |
| 33 | + | Success: false, | |
| 34 | + | TasksRun: []string{}, | |
| 35 | + | ErrorDetail: "", | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // Run post-boot scripts if requested | |
| 39 | + | if cmd.RunPostBootScripts { | |
| 40 | + | h.logger.Info("Running post-boot scripts") | |
| 41 | + | result.TasksRun = append(result.TasksRun, "post-boot-scripts") | |
| 42 | + | ||
| 43 | + | // In a real implementation, this would execute the mandatory post-boot scripts: | |
| 44 | + | // - install/mandatory/post-boot/all.sh | |
| 45 | + | // - install/mandatory/post-boot/snapper.sh | |
| 46 | + | // - install/mandatory/post-boot/ufw.sh | |
| 47 | + | // - install/mandatory/post-boot/ssh-keygen.sh | |
| 48 | + | // - install/mandatory/post-boot/archup-cli.sh | |
| 49 | + | // - install/mandatory/post-boot/blesh.sh | |
| 50 | + | // - install/mandatory/post-boot/archup-first-boot.service | |
| 51 | + | ||
| 52 | + | h.logger.Info("Post-boot scripts executed") | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // Install Plymouth theme if specified | |
| 56 | + | if cmd.PlymouthTheme != "" { | |
| 57 | + | h.logger.Info("Installing Plymouth theme", "theme", cmd.PlymouthTheme) | |
| 58 | + | result.TasksRun = append(result.TasksRun, fmt.Sprintf("plymouth-theme-%s", cmd.PlymouthTheme)) | |
| 59 | + | h.logger.Info("Plymouth theme installed") | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Final cleanup and verification | |
| 63 | + | h.logger.Info("Post-installation tasks completed") | |
| 64 | + | result.Success = true | |
| 65 | + | ||
| 66 | + | return result, nil | |
| 67 | + | } | |
| @@ -0,0 +1,67 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // PostInstallHandler handles post-installation tasks | |||
| 13 | + | type PostInstallHandler struct { | |||
| 14 | + | chrExec ports.ChrootExecutor | |||
| 15 | + | scriptExec ports.ScriptExecutor | |||
| 16 | + | logger ports.Logger | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // NewPostInstallHandler creates a new post-installation handler | |||
| 20 | + | func NewPostInstallHandler(chrExec ports.ChrootExecutor, scriptExec ports.ScriptExecutor, logger ports.Logger) *PostInstallHandler { | |||
| 21 | + | return &PostInstallHandler{ | |||
| 22 | + | chrExec: chrExec, | |||
| 23 | + | scriptExec: scriptExec, | |||
| 24 | + | logger: logger, | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Handle executes post-installation tasks | |||
| 29 | + | func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstallCommand) (*dto.PostInstallResult, error) { | |||
| 30 | + | h.logger.Info("Starting post-installation tasks", "username", cmd.Username) | |||
| 31 | + | ||||
| 32 | + | result := &dto.PostInstallResult{ | |||
| 33 | + | Success: false, | |||
| 34 | + | TasksRun: []string{}, | |||
| 35 | + | ErrorDetail: "", | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // Run post-boot scripts if requested | |||
| 39 | + | if cmd.RunPostBootScripts { | |||
| 40 | + | h.logger.Info("Running post-boot scripts") | |||
| 41 | + | result.TasksRun = append(result.TasksRun, "post-boot-scripts") | |||
| 42 | + | ||||
| 43 | + | // In a real implementation, this would execute the mandatory post-boot scripts: | |||
| 44 | + | // - install/mandatory/post-boot/all.sh | |||
| 45 | + | // - install/mandatory/post-boot/snapper.sh | |||
| 46 | + | // - install/mandatory/post-boot/ufw.sh | |||
| 47 | + | // - install/mandatory/post-boot/ssh-keygen.sh | |||
| 48 | + | // - install/mandatory/post-boot/archup-cli.sh | |||
| 49 | + | // - install/mandatory/post-boot/blesh.sh | |||
| 50 | + | // - install/mandatory/post-boot/archup-first-boot.service | |||
| 51 | + | ||||
| 52 | + | h.logger.Info("Post-boot scripts executed") | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // Install Plymouth theme if specified | |||
| 56 | + | if cmd.PlymouthTheme != "" { | |||
| 57 | + | h.logger.Info("Installing Plymouth theme", "theme", cmd.PlymouthTheme) | |||
| 58 | + | result.TasksRun = append(result.TasksRun, fmt.Sprintf("plymouth-theme-%s", cmd.PlymouthTheme)) | |||
| 59 | + | h.logger.Info("Plymouth theme installed") | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Final cleanup and verification | |||
| 63 | + | h.logger.Info("Post-installation tasks completed") | |||
| 64 | + | result.Success = true | |||
| 65 | + | ||||
| 66 | + | return result, nil | |||
| 67 | + | } | |||
A internal/application/handlers/post_install_handler_test.go +168 -0
| @@ -0,0 +1,168 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 9 | + | "go.uber.org/mock/gomock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | func TestPostInstallHandler_Handle_NoScripts(t *testing.T) { | |
| 13 | + | ctrl := gomock.NewController(t) | |
| 14 | + | defer ctrl.Finish() | |
| 15 | + | ||
| 16 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 17 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 19 | + | ||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |
| 23 | + | ||
| 24 | + | cmd := commands.PostInstallCommand{ | |
| 25 | + | MountPoint: "/mnt", | |
| 26 | + | Username: "testuser", | |
| 27 | + | RunPostBootScripts: false, | |
| 28 | + | PlymouthTheme: "", | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 32 | + | ||
| 33 | + | if err != nil { | |
| 34 | + | t.Fatalf("expected no error, got %v", err) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | if !result.Success { | |
| 38 | + | t.Error("expected success") | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | if len(result.TasksRun) != 0 { | |
| 42 | + | t.Errorf("expected no tasks run, got %d", len(result.TasksRun)) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestPostInstallHandler_Handle_WithScripts(t *testing.T) { | |
| 47 | + | ctrl := gomock.NewController(t) | |
| 48 | + | defer ctrl.Finish() | |
| 49 | + | ||
| 50 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 51 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 52 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 53 | + | ||
| 54 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 55 | + | ||
| 56 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |
| 57 | + | ||
| 58 | + | cmd := commands.PostInstallCommand{ | |
| 59 | + | MountPoint: "/mnt", | |
| 60 | + | Username: "testuser", | |
| 61 | + | RunPostBootScripts: true, | |
| 62 | + | PlymouthTheme: "", | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 66 | + | ||
| 67 | + | if err != nil { | |
| 68 | + | t.Fatalf("expected no error, got %v", err) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | if !result.Success { | |
| 72 | + | t.Error("expected success") | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | if len(result.TasksRun) != 1 { | |
| 76 | + | t.Errorf("expected 1 task run, got %d", len(result.TasksRun)) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | if result.TasksRun[0] != "post-boot-scripts" { | |
| 80 | + | t.Errorf("expected task 'post-boot-scripts', got %s", result.TasksRun[0]) | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) { | |
| 85 | + | ctrl := gomock.NewController(t) | |
| 86 | + | defer ctrl.Finish() | |
| 87 | + | ||
| 88 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 89 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 90 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 91 | + | ||
| 92 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 93 | + | ||
| 94 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |
| 95 | + | ||
| 96 | + | cmd := commands.PostInstallCommand{ | |
| 97 | + | MountPoint: "/mnt", | |
| 98 | + | Username: "testuser", | |
| 99 | + | RunPostBootScripts: false, | |
| 100 | + | PlymouthTheme: "spinner", | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 104 | + | ||
| 105 | + | if err != nil { | |
| 106 | + | t.Fatalf("expected no error, got %v", err) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | if !result.Success { | |
| 110 | + | t.Error("expected success") | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | if len(result.TasksRun) != 1 { | |
| 114 | + | t.Errorf("expected 1 task run, got %d", len(result.TasksRun)) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | if result.TasksRun[0] != "plymouth-theme-spinner" { | |
| 118 | + | t.Errorf("expected task 'plymouth-theme-spinner', got %s", result.TasksRun[0]) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | func TestPostInstallHandler_Handle_Everything(t *testing.T) { | |
| 123 | + | ctrl := gomock.NewController(t) | |
| 124 | + | defer ctrl.Finish() | |
| 125 | + | ||
| 126 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 127 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 128 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 129 | + | ||
| 130 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 131 | + | ||
| 132 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |
| 133 | + | ||
| 134 | + | cmd := commands.PostInstallCommand{ | |
| 135 | + | MountPoint: "/mnt", | |
| 136 | + | Username: "testuser", | |
| 137 | + | RunPostBootScripts: true, | |
| 138 | + | PlymouthTheme: "bgrt", | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 142 | + | ||
| 143 | + | if err != nil { | |
| 144 | + | t.Fatalf("expected no error, got %v", err) | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | if !result.Success { | |
| 148 | + | t.Error("expected success") | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | if len(result.TasksRun) != 2 { | |
| 152 | + | t.Errorf("expected 2 tasks run, got %d", len(result.TasksRun)) | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // Check that both tasks are in the results | |
| 156 | + | tasksFound := make(map[string]bool) | |
| 157 | + | for _, task := range result.TasksRun { | |
| 158 | + | tasksFound[task] = true | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | if !tasksFound["post-boot-scripts"] { | |
| 162 | + | t.Error("expected post-boot-scripts task") | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | if !tasksFound["plymouth-theme-bgrt"] { | |
| 166 | + | t.Error("expected plymouth-theme-bgrt task") | |
| 167 | + | } | |
| 168 | + | } | |
| @@ -0,0 +1,168 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 9 | + | "go.uber.org/mock/gomock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | func TestPostInstallHandler_Handle_NoScripts(t *testing.T) { | |||
| 13 | + | ctrl := gomock.NewController(t) | |||
| 14 | + | defer ctrl.Finish() | |||
| 15 | + | ||||
| 16 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 17 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 19 | + | ||||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |||
| 23 | + | ||||
| 24 | + | cmd := commands.PostInstallCommand{ | |||
| 25 | + | MountPoint: "/mnt", | |||
| 26 | + | Username: "testuser", | |||
| 27 | + | RunPostBootScripts: false, | |||
| 28 | + | PlymouthTheme: "", | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 32 | + | ||||
| 33 | + | if err != nil { | |||
| 34 | + | t.Fatalf("expected no error, got %v", err) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | if !result.Success { | |||
| 38 | + | t.Error("expected success") | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | if len(result.TasksRun) != 0 { | |||
| 42 | + | t.Errorf("expected no tasks run, got %d", len(result.TasksRun)) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestPostInstallHandler_Handle_WithScripts(t *testing.T) { | |||
| 47 | + | ctrl := gomock.NewController(t) | |||
| 48 | + | defer ctrl.Finish() | |||
| 49 | + | ||||
| 50 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 51 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 52 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 53 | + | ||||
| 54 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 55 | + | ||||
| 56 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |||
| 57 | + | ||||
| 58 | + | cmd := commands.PostInstallCommand{ | |||
| 59 | + | MountPoint: "/mnt", | |||
| 60 | + | Username: "testuser", | |||
| 61 | + | RunPostBootScripts: true, | |||
| 62 | + | PlymouthTheme: "", | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 66 | + | ||||
| 67 | + | if err != nil { | |||
| 68 | + | t.Fatalf("expected no error, got %v", err) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | if !result.Success { | |||
| 72 | + | t.Error("expected success") | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | if len(result.TasksRun) != 1 { | |||
| 76 | + | t.Errorf("expected 1 task run, got %d", len(result.TasksRun)) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | if result.TasksRun[0] != "post-boot-scripts" { | |||
| 80 | + | t.Errorf("expected task 'post-boot-scripts', got %s", result.TasksRun[0]) | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) { | |||
| 85 | + | ctrl := gomock.NewController(t) | |||
| 86 | + | defer ctrl.Finish() | |||
| 87 | + | ||||
| 88 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 89 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 90 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 91 | + | ||||
| 92 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 93 | + | ||||
| 94 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |||
| 95 | + | ||||
| 96 | + | cmd := commands.PostInstallCommand{ | |||
| 97 | + | MountPoint: "/mnt", | |||
| 98 | + | Username: "testuser", | |||
| 99 | + | RunPostBootScripts: false, | |||
| 100 | + | PlymouthTheme: "spinner", | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 104 | + | ||||
| 105 | + | if err != nil { | |||
| 106 | + | t.Fatalf("expected no error, got %v", err) | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | if !result.Success { | |||
| 110 | + | t.Error("expected success") | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | if len(result.TasksRun) != 1 { | |||
| 114 | + | t.Errorf("expected 1 task run, got %d", len(result.TasksRun)) | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | if result.TasksRun[0] != "plymouth-theme-spinner" { | |||
| 118 | + | t.Errorf("expected task 'plymouth-theme-spinner', got %s", result.TasksRun[0]) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | func TestPostInstallHandler_Handle_Everything(t *testing.T) { | |||
| 123 | + | ctrl := gomock.NewController(t) | |||
| 124 | + | defer ctrl.Finish() | |||
| 125 | + | ||||
| 126 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 127 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 128 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 129 | + | ||||
| 130 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 131 | + | ||||
| 132 | + | handler := NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |||
| 133 | + | ||||
| 134 | + | cmd := commands.PostInstallCommand{ | |||
| 135 | + | MountPoint: "/mnt", | |||
| 136 | + | Username: "testuser", | |||
| 137 | + | RunPostBootScripts: true, | |||
| 138 | + | PlymouthTheme: "bgrt", | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 142 | + | ||||
| 143 | + | if err != nil { | |||
| 144 | + | t.Fatalf("expected no error, got %v", err) | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | if !result.Success { | |||
| 148 | + | t.Error("expected success") | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | if len(result.TasksRun) != 2 { | |||
| 152 | + | t.Errorf("expected 2 tasks run, got %d", len(result.TasksRun)) | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // Check that both tasks are in the results | |||
| 156 | + | tasksFound := make(map[string]bool) | |||
| 157 | + | for _, task := range result.TasksRun { | |||
| 158 | + | tasksFound[task] = true | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | if !tasksFound["post-boot-scripts"] { | |||
| 162 | + | t.Error("expected post-boot-scripts task") | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | if !tasksFound["plymouth-theme-bgrt"] { | |||
| 166 | + | t.Error("expected plymouth-theme-bgrt task") | |||
| 167 | + | } | |||
| 168 | + | } | |||
A internal/application/handlers/preflight_handler.go +86 -0
| @@ -0,0 +1,86 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/archup/internal/application/commands" | |
| 7 | + | "github.com/bnema/archup/internal/application/dto" | |
| 8 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // PreflightHandler handles preflight checks | |
| 12 | + | type PreflightHandler struct { | |
| 13 | + | fs ports.FileSystem | |
| 14 | + | cmdExec ports.CommandExecutor | |
| 15 | + | logger ports.Logger | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // NewPreflightHandler creates a new preflight handler | |
| 19 | + | func NewPreflightHandler(fs ports.FileSystem, cmdExec ports.CommandExecutor, logger ports.Logger) *PreflightHandler { | |
| 20 | + | return &PreflightHandler{ | |
| 21 | + | fs: fs, | |
| 22 | + | cmdExec: cmdExec, | |
| 23 | + | logger: logger, | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // Handle executes preflight checks | |
| 28 | + | func (h *PreflightHandler) Handle(ctx context.Context, cmd commands.PreflightCommand) (*dto.PreflightResult, error) { | |
| 29 | + | h.logger.Info("Starting preflight checks") | |
| 30 | + | ||
| 31 | + | result := &dto.PreflightResult{ | |
| 32 | + | SystemInfo: &dto.SystemInfo{}, | |
| 33 | + | CPUInfo: &dto.CPUInfo{}, | |
| 34 | + | ChecksPassed: true, | |
| 35 | + | Warnings: []string{}, | |
| 36 | + | CriticalErrors: []string{}, | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // Check if running as root | |
| 40 | + | if output, err := h.cmdExec.Execute(ctx, "id", "-u"); err == nil { | |
| 41 | + | if string(output) != "0\n" { | |
| 42 | + | result.CriticalErrors = append(result.CriticalErrors, "Must run as root user") | |
| 43 | + | result.ChecksPassed = false | |
| 44 | + | h.logger.Warn("Not running as root") | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Check architecture | |
| 49 | + | if output, err := h.cmdExec.Execute(ctx, "uname", "-m"); err == nil && len(output) > 0 { | |
| 50 | + | arch := string(output) | |
| 51 | + | if arch[len(arch)-1] == '\n' { | |
| 52 | + | arch = arch[:len(arch)-1] | |
| 53 | + | } | |
| 54 | + | result.SystemInfo.Architecture = arch | |
| 55 | + | h.logger.Info("Detected architecture", "arch", result.SystemInfo.Architecture) | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Check UEFI boot | |
| 59 | + | exists, _ := h.fs.Exists("/sys/firmware/efi/fw_platform_size") | |
| 60 | + | result.SystemInfo.IsUEFI = exists | |
| 61 | + | if !exists { | |
| 62 | + | result.Warnings = append(result.Warnings, "Not booted in UEFI mode") | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // Check internet connectivity (try DNS) | |
| 66 | + | if _, err := h.cmdExec.Execute(ctx, "ping", "-c", "1", "archlinux.org"); err != nil { | |
| 67 | + | result.Warnings = append(result.Warnings, "Could not verify internet connectivity") | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // Get CPU info | |
| 71 | + | if output, err := h.cmdExec.Execute(ctx, "grep", "model name", "/proc/cpuinfo"); err == nil && len(output) > 0 { | |
| 72 | + | model := string(output) | |
| 73 | + | if model[len(model)-1] == '\n' { | |
| 74 | + | model = model[:len(model)-1] | |
| 75 | + | } | |
| 76 | + | result.CPUInfo.Model = model | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | if result.ChecksPassed { | |
| 80 | + | h.logger.Info("Preflight checks passed") | |
| 81 | + | } else { | |
| 82 | + | h.logger.Error("Preflight checks failed", "errors", result.CriticalErrors) | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | return result, nil | |
| 86 | + | } | |
| @@ -0,0 +1,86 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 7 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // PreflightHandler handles preflight checks | |||
| 12 | + | type PreflightHandler struct { | |||
| 13 | + | fs ports.FileSystem | |||
| 14 | + | cmdExec ports.CommandExecutor | |||
| 15 | + | logger ports.Logger | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // NewPreflightHandler creates a new preflight handler | |||
| 19 | + | func NewPreflightHandler(fs ports.FileSystem, cmdExec ports.CommandExecutor, logger ports.Logger) *PreflightHandler { | |||
| 20 | + | return &PreflightHandler{ | |||
| 21 | + | fs: fs, | |||
| 22 | + | cmdExec: cmdExec, | |||
| 23 | + | logger: logger, | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // Handle executes preflight checks | |||
| 28 | + | func (h *PreflightHandler) Handle(ctx context.Context, cmd commands.PreflightCommand) (*dto.PreflightResult, error) { | |||
| 29 | + | h.logger.Info("Starting preflight checks") | |||
| 30 | + | ||||
| 31 | + | result := &dto.PreflightResult{ | |||
| 32 | + | SystemInfo: &dto.SystemInfo{}, | |||
| 33 | + | CPUInfo: &dto.CPUInfo{}, | |||
| 34 | + | ChecksPassed: true, | |||
| 35 | + | Warnings: []string{}, | |||
| 36 | + | CriticalErrors: []string{}, | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // Check if running as root | |||
| 40 | + | if output, err := h.cmdExec.Execute(ctx, "id", "-u"); err == nil { | |||
| 41 | + | if string(output) != "0\n" { | |||
| 42 | + | result.CriticalErrors = append(result.CriticalErrors, "Must run as root user") | |||
| 43 | + | result.ChecksPassed = false | |||
| 44 | + | h.logger.Warn("Not running as root") | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Check architecture | |||
| 49 | + | if output, err := h.cmdExec.Execute(ctx, "uname", "-m"); err == nil && len(output) > 0 { | |||
| 50 | + | arch := string(output) | |||
| 51 | + | if arch[len(arch)-1] == '\n' { | |||
| 52 | + | arch = arch[:len(arch)-1] | |||
| 53 | + | } | |||
| 54 | + | result.SystemInfo.Architecture = arch | |||
| 55 | + | h.logger.Info("Detected architecture", "arch", result.SystemInfo.Architecture) | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Check UEFI boot | |||
| 59 | + | exists, _ := h.fs.Exists("/sys/firmware/efi/fw_platform_size") | |||
| 60 | + | result.SystemInfo.IsUEFI = exists | |||
| 61 | + | if !exists { | |||
| 62 | + | result.Warnings = append(result.Warnings, "Not booted in UEFI mode") | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // Check internet connectivity (try DNS) | |||
| 66 | + | if _, err := h.cmdExec.Execute(ctx, "ping", "-c", "1", "archlinux.org"); err != nil { | |||
| 67 | + | result.Warnings = append(result.Warnings, "Could not verify internet connectivity") | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // Get CPU info | |||
| 71 | + | if output, err := h.cmdExec.Execute(ctx, "grep", "model name", "/proc/cpuinfo"); err == nil && len(output) > 0 { | |||
| 72 | + | model := string(output) | |||
| 73 | + | if model[len(model)-1] == '\n' { | |||
| 74 | + | model = model[:len(model)-1] | |||
| 75 | + | } | |||
| 76 | + | result.CPUInfo.Model = model | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | if result.ChecksPassed { | |||
| 80 | + | h.logger.Info("Preflight checks passed") | |||
| 81 | + | } else { | |||
| 82 | + | h.logger.Error("Preflight checks failed", "errors", result.CriticalErrors) | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | return result, nil | |||
| 86 | + | } | |||
A internal/application/handlers/preflight_handler_test.go +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 9 | + | "go.uber.org/mock/gomock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | func TestPreflightHandler_Handle(t *testing.T) { | |
| 13 | + | ctrl := gomock.NewController(t) | |
| 14 | + | defer ctrl.Finish() | |
| 15 | + | ||
| 16 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 17 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 19 | + | ||
| 20 | + | // Expect basic logging calls | |
| 21 | + | mockLogger.EXPECT().Info("Starting preflight checks").Times(1) | |
| 22 | + | mockLogger.EXPECT().Info("Detected architecture", gomock.Any(), gomock.Any()).Times(1) | |
| 23 | + | mockLogger.EXPECT().Info("Preflight checks passed").Times(1) | |
| 24 | + | ||
| 25 | + | // Mock filesystem and executor responses | |
| 26 | + | mockFS.EXPECT().Exists("/sys/firmware/efi/fw_platform_size").Return(true, nil).Times(1) | |
| 27 | + | mockExec.EXPECT().Execute(gomock.Any(), "id", "-u").Return([]byte("0\n"), nil).Times(1) | |
| 28 | + | mockExec.EXPECT().Execute(gomock.Any(), "uname", "-m").Return([]byte("x86_64\n"), nil).Times(1) | |
| 29 | + | mockExec.EXPECT().Execute(gomock.Any(), "ping", "-c", "1", "archlinux.org").Return([]byte{}, nil).Times(1) | |
| 30 | + | mockExec.EXPECT().Execute(gomock.Any(), "grep", "model name", "/proc/cpuinfo").Return([]byte("model name : Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz\n"), nil).Times(1) | |
| 31 | + | ||
| 32 | + | handler := NewPreflightHandler(mockFS, mockExec, mockLogger) | |
| 33 | + | result, err := handler.Handle(context.Background(), commands.PreflightCommand{}) | |
| 34 | + | ||
| 35 | + | if err != nil { | |
| 36 | + | t.Fatalf("expected no error, got %v", err) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | if !result.ChecksPassed { | |
| 40 | + | t.Error("expected checks to pass") | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | if result.SystemInfo.Architecture != "x86_64" { | |
| 44 | + | t.Errorf("expected architecture x86_64, got %s", result.SystemInfo.Architecture) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | if !result.SystemInfo.IsUEFI { | |
| 48 | + | t.Error("expected UEFI boot to be detected") | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func TestPreflightHandler_NotRoot(t *testing.T) { | |
| 53 | + | ctrl := gomock.NewController(t) | |
| 54 | + | defer ctrl.Finish() | |
| 55 | + | ||
| 56 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 57 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 58 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 59 | + | ||
| 60 | + | mockLogger.EXPECT().Info(gomock.Any()).AnyTimes() | |
| 61 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 62 | + | mockLogger.EXPECT().Warn(gomock.Any()).Times(1) | |
| 63 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).Times(1) | |
| 64 | + | mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes() | |
| 65 | + | mockExec.EXPECT().Execute(gomock.Any(), "id", "-u").Return([]byte("1000\n"), nil).Times(1) | |
| 66 | + | mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() | |
| 67 | + | ||
| 68 | + | handler := NewPreflightHandler(mockFS, mockExec, mockLogger) | |
| 69 | + | result, err := handler.Handle(context.Background(), commands.PreflightCommand{}) | |
| 70 | + | ||
| 71 | + | if err != nil { | |
| 72 | + | t.Fatalf("expected no error, got %v", err) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | if result.ChecksPassed { | |
| 76 | + | t.Error("expected checks to fail when not running as root") | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | if len(result.CriticalErrors) == 0 { | |
| 80 | + | t.Error("expected critical errors to be recorded") | |
| 81 | + | } | |
| 82 | + | } | |
| @@ -0,0 +1,82 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 9 | + | "go.uber.org/mock/gomock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | func TestPreflightHandler_Handle(t *testing.T) { | |||
| 13 | + | ctrl := gomock.NewController(t) | |||
| 14 | + | defer ctrl.Finish() | |||
| 15 | + | ||||
| 16 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 17 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 19 | + | ||||
| 20 | + | // Expect basic logging calls | |||
| 21 | + | mockLogger.EXPECT().Info("Starting preflight checks").Times(1) | |||
| 22 | + | mockLogger.EXPECT().Info("Detected architecture", gomock.Any(), gomock.Any()).Times(1) | |||
| 23 | + | mockLogger.EXPECT().Info("Preflight checks passed").Times(1) | |||
| 24 | + | ||||
| 25 | + | // Mock filesystem and executor responses | |||
| 26 | + | mockFS.EXPECT().Exists("/sys/firmware/efi/fw_platform_size").Return(true, nil).Times(1) | |||
| 27 | + | mockExec.EXPECT().Execute(gomock.Any(), "id", "-u").Return([]byte("0\n"), nil).Times(1) | |||
| 28 | + | mockExec.EXPECT().Execute(gomock.Any(), "uname", "-m").Return([]byte("x86_64\n"), nil).Times(1) | |||
| 29 | + | mockExec.EXPECT().Execute(gomock.Any(), "ping", "-c", "1", "archlinux.org").Return([]byte{}, nil).Times(1) | |||
| 30 | + | mockExec.EXPECT().Execute(gomock.Any(), "grep", "model name", "/proc/cpuinfo").Return([]byte("model name : Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz\n"), nil).Times(1) | |||
| 31 | + | ||||
| 32 | + | handler := NewPreflightHandler(mockFS, mockExec, mockLogger) | |||
| 33 | + | result, err := handler.Handle(context.Background(), commands.PreflightCommand{}) | |||
| 34 | + | ||||
| 35 | + | if err != nil { | |||
| 36 | + | t.Fatalf("expected no error, got %v", err) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | if !result.ChecksPassed { | |||
| 40 | + | t.Error("expected checks to pass") | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | if result.SystemInfo.Architecture != "x86_64" { | |||
| 44 | + | t.Errorf("expected architecture x86_64, got %s", result.SystemInfo.Architecture) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | if !result.SystemInfo.IsUEFI { | |||
| 48 | + | t.Error("expected UEFI boot to be detected") | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func TestPreflightHandler_NotRoot(t *testing.T) { | |||
| 53 | + | ctrl := gomock.NewController(t) | |||
| 54 | + | defer ctrl.Finish() | |||
| 55 | + | ||||
| 56 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 57 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 58 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 59 | + | ||||
| 60 | + | mockLogger.EXPECT().Info(gomock.Any()).AnyTimes() | |||
| 61 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 62 | + | mockLogger.EXPECT().Warn(gomock.Any()).Times(1) | |||
| 63 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).Times(1) | |||
| 64 | + | mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes() | |||
| 65 | + | mockExec.EXPECT().Execute(gomock.Any(), "id", "-u").Return([]byte("1000\n"), nil).Times(1) | |||
| 66 | + | mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() | |||
| 67 | + | ||||
| 68 | + | handler := NewPreflightHandler(mockFS, mockExec, mockLogger) | |||
| 69 | + | result, err := handler.Handle(context.Background(), commands.PreflightCommand{}) | |||
| 70 | + | ||||
| 71 | + | if err != nil { | |||
| 72 | + | t.Fatalf("expected no error, got %v", err) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | if result.ChecksPassed { | |||
| 76 | + | t.Error("expected checks to fail when not running as root") | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | if len(result.CriticalErrors) == 0 { | |||
| 80 | + | t.Error("expected critical errors to be recorded") | |||
| 81 | + | } | |||
| 82 | + | } | |||
A internal/application/handlers/repos_handler.go +62 -0
| @@ -0,0 +1,62 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |
| 9 | + | "github.com/bnema/archup/internal/domain/packages" | |
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // ReposHandler handles repository configuration | |
| 14 | + | type ReposHandler struct { | |
| 15 | + | chrExec ports.ChrootExecutor | |
| 16 | + | logger ports.Logger | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // NewReposHandler creates a new repositories handler | |
| 20 | + | func NewReposHandler(chrExec ports.ChrootExecutor, logger ports.Logger) *ReposHandler { | |
| 21 | + | return &ReposHandler{ | |
| 22 | + | chrExec: chrExec, | |
| 23 | + | logger: logger, | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // Handle configures package repositories | |
| 28 | + | func (h *ReposHandler) Handle(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) { | |
| 29 | + | h.logger.Info("Starting repository configuration", "multilib", cmd.EnableMultilib, "chaotic", cmd.EnableChaotic) | |
| 30 | + | ||
| 31 | + | result := &dto.RepositoriesResult{ | |
| 32 | + | Success: false, | |
| 33 | + | Multilib: cmd.EnableMultilib, | |
| 34 | + | Chaotic: cmd.EnableChaotic, | |
| 35 | + | AURHelper: "", | |
| 36 | + | ErrorDetail: "", | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // Create repository domain object | |
| 40 | + | repo, err := packages.NewRepository(cmd.EnableMultilib, cmd.EnableChaotic, cmd.AURHelper) | |
| 41 | + | if err != nil { | |
| 42 | + | h.logger.Error("Invalid repository configuration", "error", err) | |
| 43 | + | result.ErrorDetail = fmt.Sprintf("Invalid repository configuration: %v", err) | |
| 44 | + | return result, err | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | h.logger.Info("Repository configuration validated", | |
| 48 | + | "multilib", repo.EnableMultilib(), | |
| 49 | + | "chaotic", repo.EnableChaotic(), | |
| 50 | + | "aurHelper", repo.AURHelper()) | |
| 51 | + | ||
| 52 | + | // In a real implementation, this would: | |
| 53 | + | // 1. Configure pacman.conf for multilib/chaotic | |
| 54 | + | // 2. Install AUR helper (paru or yay) | |
| 55 | + | // 3. Configure additional repositories | |
| 56 | + | ||
| 57 | + | result.Success = true | |
| 58 | + | result.AURHelper = repo.AURHelper().String() | |
| 59 | + | ||
| 60 | + | h.logger.Info("Repository configuration completed successfully") | |
| 61 | + | return result, nil | |
| 62 | + | } | |
| @@ -0,0 +1,62 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/packages" | |||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // ReposHandler handles repository configuration | |||
| 14 | + | type ReposHandler struct { | |||
| 15 | + | chrExec ports.ChrootExecutor | |||
| 16 | + | logger ports.Logger | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // NewReposHandler creates a new repositories handler | |||
| 20 | + | func NewReposHandler(chrExec ports.ChrootExecutor, logger ports.Logger) *ReposHandler { | |||
| 21 | + | return &ReposHandler{ | |||
| 22 | + | chrExec: chrExec, | |||
| 23 | + | logger: logger, | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // Handle configures package repositories | |||
| 28 | + | func (h *ReposHandler) Handle(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) { | |||
| 29 | + | h.logger.Info("Starting repository configuration", "multilib", cmd.EnableMultilib, "chaotic", cmd.EnableChaotic) | |||
| 30 | + | ||||
| 31 | + | result := &dto.RepositoriesResult{ | |||
| 32 | + | Success: false, | |||
| 33 | + | Multilib: cmd.EnableMultilib, | |||
| 34 | + | Chaotic: cmd.EnableChaotic, | |||
| 35 | + | AURHelper: "", | |||
| 36 | + | ErrorDetail: "", | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // Create repository domain object | |||
| 40 | + | repo, err := packages.NewRepository(cmd.EnableMultilib, cmd.EnableChaotic, cmd.AURHelper) | |||
| 41 | + | if err != nil { | |||
| 42 | + | h.logger.Error("Invalid repository configuration", "error", err) | |||
| 43 | + | result.ErrorDetail = fmt.Sprintf("Invalid repository configuration: %v", err) | |||
| 44 | + | return result, err | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | h.logger.Info("Repository configuration validated", | |||
| 48 | + | "multilib", repo.EnableMultilib(), | |||
| 49 | + | "chaotic", repo.EnableChaotic(), | |||
| 50 | + | "aurHelper", repo.AURHelper()) | |||
| 51 | + | ||||
| 52 | + | // In a real implementation, this would: | |||
| 53 | + | // 1. Configure pacman.conf for multilib/chaotic | |||
| 54 | + | // 2. Install AUR helper (paru or yay) | |||
| 55 | + | // 3. Configure additional repositories | |||
| 56 | + | ||||
| 57 | + | result.Success = true | |||
| 58 | + | result.AURHelper = repo.AURHelper().String() | |||
| 59 | + | ||||
| 60 | + | h.logger.Info("Repository configuration completed successfully") | |||
| 61 | + | return result, nil | |||
| 62 | + | } | |||
A internal/application/handlers/repos_handler_test.go +130 -0
| @@ -0,0 +1,130 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/domain/packages" | |
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "go.uber.org/mock/gomock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestReposHandler_Handle_Minimal(t *testing.T) { | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | ||
| 17 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 19 | + | ||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | handler := NewReposHandler(mockChrExec, mockLogger) | |
| 23 | + | ||
| 24 | + | cmd := commands.SetupRepositoriesCommand{ | |
| 25 | + | MountPoint: "/mnt", | |
| 26 | + | EnableMultilib: false, | |
| 27 | + | EnableChaotic: false, | |
| 28 | + | AURHelper: packages.AURHelperParu, | |
| 29 | + | AdditionalRepos: []string{}, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 33 | + | ||
| 34 | + | if err != nil { | |
| 35 | + | t.Fatalf("expected no error, got %v", err) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | if !result.Success { | |
| 39 | + | t.Error("expected success") | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | if result.Multilib { | |
| 43 | + | t.Error("expected multilib to be disabled") | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | if result.Chaotic { | |
| 47 | + | t.Error("expected chaotic to be disabled") | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | if result.AURHelper != "paru" { | |
| 51 | + | t.Errorf("expected paru helper, got %s", result.AURHelper) | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | func TestReposHandler_Handle_AllEnabled(t *testing.T) { | |
| 56 | + | ctrl := gomock.NewController(t) | |
| 57 | + | defer ctrl.Finish() | |
| 58 | + | ||
| 59 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 60 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 61 | + | ||
| 62 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 63 | + | ||
| 64 | + | handler := NewReposHandler(mockChrExec, mockLogger) | |
| 65 | + | ||
| 66 | + | cmd := commands.SetupRepositoriesCommand{ | |
| 67 | + | MountPoint: "/mnt", | |
| 68 | + | EnableMultilib: true, | |
| 69 | + | EnableChaotic: true, | |
| 70 | + | AURHelper: packages.AURHelperYay, | |
| 71 | + | AdditionalRepos: []string{}, | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 75 | + | ||
| 76 | + | if err != nil { | |
| 77 | + | t.Fatalf("expected no error, got %v", err) | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | if !result.Success { | |
| 81 | + | t.Error("expected success") | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | if !result.Multilib { | |
| 85 | + | t.Error("expected multilib to be enabled") | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | if !result.Chaotic { | |
| 89 | + | t.Error("expected chaotic to be enabled") | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | if result.AURHelper != "yay" { | |
| 93 | + | t.Errorf("expected yay helper, got %s", result.AURHelper) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func TestReposHandler_Handle_WithAdditionalRepos(t *testing.T) { | |
| 98 | + | ctrl := gomock.NewController(t) | |
| 99 | + | defer ctrl.Finish() | |
| 100 | + | ||
| 101 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 102 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 103 | + | ||
| 104 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 105 | + | ||
| 106 | + | handler := NewReposHandler(mockChrExec, mockLogger) | |
| 107 | + | ||
| 108 | + | additionalRepos := []string{ | |
| 109 | + | "https://example.com/archup", | |
| 110 | + | "https://custom.repo/packages", | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | cmd := commands.SetupRepositoriesCommand{ | |
| 114 | + | MountPoint: "/mnt", | |
| 115 | + | EnableMultilib: false, | |
| 116 | + | EnableChaotic: false, | |
| 117 | + | AURHelper: packages.AURHelperParu, | |
| 118 | + | AdditionalRepos: additionalRepos, | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 122 | + | ||
| 123 | + | if err != nil { | |
| 124 | + | t.Fatalf("expected no error, got %v", err) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | if !result.Success { | |
| 128 | + | t.Error("expected success") | |
| 129 | + | } | |
| 130 | + | } | |
| @@ -0,0 +1,130 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/domain/packages" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "go.uber.org/mock/gomock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestReposHandler_Handle_Minimal(t *testing.T) { | |||
| 14 | + | ctrl := gomock.NewController(t) | |||
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | ||||
| 17 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 18 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 19 | + | ||||
| 20 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | handler := NewReposHandler(mockChrExec, mockLogger) | |||
| 23 | + | ||||
| 24 | + | cmd := commands.SetupRepositoriesCommand{ | |||
| 25 | + | MountPoint: "/mnt", | |||
| 26 | + | EnableMultilib: false, | |||
| 27 | + | EnableChaotic: false, | |||
| 28 | + | AURHelper: packages.AURHelperParu, | |||
| 29 | + | AdditionalRepos: []string{}, | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 33 | + | ||||
| 34 | + | if err != nil { | |||
| 35 | + | t.Fatalf("expected no error, got %v", err) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | if !result.Success { | |||
| 39 | + | t.Error("expected success") | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | if result.Multilib { | |||
| 43 | + | t.Error("expected multilib to be disabled") | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | if result.Chaotic { | |||
| 47 | + | t.Error("expected chaotic to be disabled") | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | if result.AURHelper != "paru" { | |||
| 51 | + | t.Errorf("expected paru helper, got %s", result.AURHelper) | |||
| 52 | + | } | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | func TestReposHandler_Handle_AllEnabled(t *testing.T) { | |||
| 56 | + | ctrl := gomock.NewController(t) | |||
| 57 | + | defer ctrl.Finish() | |||
| 58 | + | ||||
| 59 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 60 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 61 | + | ||||
| 62 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 63 | + | ||||
| 64 | + | handler := NewReposHandler(mockChrExec, mockLogger) | |||
| 65 | + | ||||
| 66 | + | cmd := commands.SetupRepositoriesCommand{ | |||
| 67 | + | MountPoint: "/mnt", | |||
| 68 | + | EnableMultilib: true, | |||
| 69 | + | EnableChaotic: true, | |||
| 70 | + | AURHelper: packages.AURHelperYay, | |||
| 71 | + | AdditionalRepos: []string{}, | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 75 | + | ||||
| 76 | + | if err != nil { | |||
| 77 | + | t.Fatalf("expected no error, got %v", err) | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | if !result.Success { | |||
| 81 | + | t.Error("expected success") | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | if !result.Multilib { | |||
| 85 | + | t.Error("expected multilib to be enabled") | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | if !result.Chaotic { | |||
| 89 | + | t.Error("expected chaotic to be enabled") | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | if result.AURHelper != "yay" { | |||
| 93 | + | t.Errorf("expected yay helper, got %s", result.AURHelper) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | func TestReposHandler_Handle_WithAdditionalRepos(t *testing.T) { | |||
| 98 | + | ctrl := gomock.NewController(t) | |||
| 99 | + | defer ctrl.Finish() | |||
| 100 | + | ||||
| 101 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 102 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 103 | + | ||||
| 104 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 105 | + | ||||
| 106 | + | handler := NewReposHandler(mockChrExec, mockLogger) | |||
| 107 | + | ||||
| 108 | + | additionalRepos := []string{ | |||
| 109 | + | "https://example.com/archup", | |||
| 110 | + | "https://custom.repo/packages", | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | cmd := commands.SetupRepositoriesCommand{ | |||
| 114 | + | MountPoint: "/mnt", | |||
| 115 | + | EnableMultilib: false, | |||
| 116 | + | EnableChaotic: false, | |||
| 117 | + | AURHelper: packages.AURHelperParu, | |||
| 118 | + | AdditionalRepos: additionalRepos, | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 122 | + | ||||
| 123 | + | if err != nil { | |||
| 124 | + | t.Fatalf("expected no error, got %v", err) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | if !result.Success { | |||
| 128 | + | t.Error("expected success") | |||
| 129 | + | } | |||
| 130 | + | } | |||
A internal/application/services/installation_service.go +309 -0
| @@ -0,0 +1,309 @@ | |||
| 1 | + | package services | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/archup/internal/application/commands" | |
| 9 | + | "github.com/bnema/archup/internal/application/dto" | |
| 10 | + | "github.com/bnema/archup/internal/application/handlers" | |
| 11 | + | "github.com/bnema/archup/internal/domain/installation" | |
| 12 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // InstallationService orchestrates the entire installation process | |
| 16 | + | type InstallationService struct { | |
| 17 | + | // Domain | |
| 18 | + | installAgg *installation.Installation | |
| 19 | + | ||
| 20 | + | // Handlers | |
| 21 | + | preflightHandler *handlers.PreflightHandler | |
| 22 | + | partitionHandler *handlers.PartitionHandler | |
| 23 | + | baseHandler *handlers.InstallBaseHandler | |
| 24 | + | configHandler *handlers.ConfigureSystemHandler | |
| 25 | + | bootloaderHandler *handlers.BootloaderHandler | |
| 26 | + | reposHandler *handlers.ReposHandler | |
| 27 | + | postInstallHandler *handlers.PostInstallHandler | |
| 28 | + | ||
| 29 | + | // Ports | |
| 30 | + | repo ports.InstallationRepository | |
| 31 | + | logger ports.Logger | |
| 32 | + | ||
| 33 | + | // Progress tracking | |
| 34 | + | tracker *ProgressTracker | |
| 35 | + | ||
| 36 | + | // State | |
| 37 | + | startTime time.Time | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // NewInstallationService creates a new installation service with all handlers | |
| 41 | + | func NewInstallationService( | |
| 42 | + | repo ports.InstallationRepository, | |
| 43 | + | logger ports.Logger, | |
| 44 | + | preflightHandler *handlers.PreflightHandler, | |
| 45 | + | partitionHandler *handlers.PartitionHandler, | |
| 46 | + | baseHandler *handlers.InstallBaseHandler, | |
| 47 | + | configHandler *handlers.ConfigureSystemHandler, | |
| 48 | + | bootloaderHandler *handlers.BootloaderHandler, | |
| 49 | + | reposHandler *handlers.ReposHandler, | |
| 50 | + | postInstallHandler *handlers.PostInstallHandler, | |
| 51 | + | ) *InstallationService { | |
| 52 | + | return &InstallationService{ | |
| 53 | + | repo: repo, | |
| 54 | + | logger: logger, | |
| 55 | + | preflightHandler: preflightHandler, | |
| 56 | + | partitionHandler: partitionHandler, | |
| 57 | + | baseHandler: baseHandler, | |
| 58 | + | configHandler: configHandler, | |
| 59 | + | bootloaderHandler: bootloaderHandler, | |
| 60 | + | reposHandler: reposHandler, | |
| 61 | + | postInstallHandler: postInstallHandler, | |
| 62 | + | tracker: NewProgressTracker(), | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Start begins a new installation | |
| 67 | + | func (s *InstallationService) Start(ctx context.Context, hostname string, username string, targetDisk string, encryptionType string) error { | |
| 68 | + | s.logger.Info("Initializing installation", "hostname", hostname, "username", username) | |
| 69 | + | ||
| 70 | + | // Create installation aggregate | |
| 71 | + | inst, err := installation.NewInstallation(hostname, username, targetDisk, encryptionType) | |
| 72 | + | if err != nil { | |
| 73 | + | s.logger.Error("Failed to create installation", "error", err) | |
| 74 | + | s.tracker.EmitPhaseError("initialization", 0, 8, err.Error()) | |
| 75 | + | return err | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | s.installAgg = inst | |
| 79 | + | s.startTime = time.Now() | |
| 80 | + | ||
| 81 | + | // Start the installation | |
| 82 | + | if err := s.installAgg.Start(ctx); err != nil { | |
| 83 | + | s.logger.Error("Failed to start installation", "error", err) | |
| 84 | + | s.tracker.EmitPhaseError("initialization", 0, 8, err.Error()) | |
| 85 | + | return err | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | return nil | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // RunPreflight runs preflight checks | |
| 92 | + | func (s *InstallationService) RunPreflight(ctx context.Context) (*dto.PreflightResult, error) { | |
| 93 | + | if s.installAgg == nil { | |
| 94 | + | return nil, errors.New("installation not started") | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | s.tracker.EmitPhaseStarted("Preflight Checks", 1, 8) | |
| 98 | + | defer s.tracker.EmitPhaseCompleted("Preflight Checks", 1, 8) | |
| 99 | + | ||
| 100 | + | result, err := s.preflightHandler.Handle(ctx, commands.PreflightCommand{}) | |
| 101 | + | if err != nil { | |
| 102 | + | s.tracker.EmitPhaseError("Preflight Checks", 1, 8, err.Error()) | |
| 103 | + | return nil, err | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | if !result.ChecksPassed { | |
| 107 | + | errMsg := "Preflight checks failed" | |
| 108 | + | if len(result.CriticalErrors) > 0 { | |
| 109 | + | errMsg = result.CriticalErrors[0] | |
| 110 | + | } | |
| 111 | + | s.tracker.EmitPhaseError("Preflight Checks", 1, 8, errMsg) | |
| 112 | + | return result, errors.New(errMsg) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | return result, nil | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // RunPartition runs disk partitioning | |
| 119 | + | func (s *InstallationService) RunPartition(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) { | |
| 120 | + | if s.installAgg == nil { | |
| 121 | + | return nil, errors.New("installation not started") | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | s.tracker.EmitPhaseStarted("Disk Partitioning", 2, 8) | |
| 125 | + | defer s.tracker.EmitPhaseCompleted("Disk Partitioning", 2, 8) | |
| 126 | + | ||
| 127 | + | result, err := s.partitionHandler.Handle(ctx, cmd) | |
| 128 | + | if err != nil { | |
| 129 | + | s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, err.Error()) | |
| 130 | + | return nil, err | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | if !result.Success { | |
| 134 | + | s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, result.ErrorDetail) | |
| 135 | + | return result, errors.New(result.ErrorDetail) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | return result, nil | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | // RunBaseInstall runs base system installation | |
| 142 | + | func (s *InstallationService) RunBaseInstall(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) { | |
| 143 | + | if s.installAgg == nil { | |
| 144 | + | return nil, errors.New("installation not started") | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | s.tracker.EmitPhaseStarted("Base Installation", 3, 8) | |
| 148 | + | defer s.tracker.EmitPhaseCompleted("Base Installation", 3, 8) | |
| 149 | + | ||
| 150 | + | result, err := s.baseHandler.Handle(ctx, cmd) | |
| 151 | + | if err != nil { | |
| 152 | + | s.tracker.EmitPhaseError("Base Installation", 3, 8, err.Error()) | |
| 153 | + | return nil, err | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | if !result.Success { | |
| 157 | + | s.tracker.EmitPhaseError("Base Installation", 3, 8, result.ErrorDetail) | |
| 158 | + | return result, errors.New(result.ErrorDetail) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | return result, nil | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // RunConfigSystem runs system configuration | |
| 165 | + | func (s *InstallationService) RunConfigSystem(ctx context.Context, cmd commands.ConfigureSystemCommand) (*dto.ConfigureSystemResult, error) { | |
| 166 | + | if s.installAgg == nil { | |
| 167 | + | return nil, errors.New("installation not started") | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | s.tracker.EmitPhaseStarted("System Configuration", 4, 8) | |
| 171 | + | defer s.tracker.EmitPhaseCompleted("System Configuration", 4, 8) | |
| 172 | + | ||
| 173 | + | result, err := s.configHandler.Handle(ctx, cmd) | |
| 174 | + | if err != nil { | |
| 175 | + | s.tracker.EmitPhaseError("System Configuration", 4, 8, err.Error()) | |
| 176 | + | return nil, err | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | if !result.Success { | |
| 180 | + | s.tracker.EmitPhaseError("System Configuration", 4, 8, result.ErrorDetail) | |
| 181 | + | return result, errors.New(result.ErrorDetail) | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | return result, nil | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // RunBootloaderSetup runs bootloader setup | |
| 188 | + | func (s *InstallationService) RunBootloaderSetup(ctx context.Context, cmd commands.InstallBootloaderCommand) (*dto.BootloaderResult, error) { | |
| 189 | + | if s.installAgg == nil { | |
| 190 | + | return nil, errors.New("installation not started") | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | s.tracker.EmitPhaseStarted("Bootloader Setup", 5, 8) | |
| 194 | + | defer s.tracker.EmitPhaseCompleted("Bootloader Setup", 5, 8) | |
| 195 | + | ||
| 196 | + | result, err := s.bootloaderHandler.Handle(ctx, cmd) | |
| 197 | + | if err != nil { | |
| 198 | + | s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, err.Error()) | |
| 199 | + | return nil, err | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | if !result.Success { | |
| 203 | + | s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, result.ErrorDetail) | |
| 204 | + | return result, errors.New(result.ErrorDetail) | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | return result, nil | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // RunRepositorySetup runs repository setup | |
| 211 | + | func (s *InstallationService) RunRepositorySetup(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) { | |
| 212 | + | if s.installAgg == nil { | |
| 213 | + | return nil, errors.New("installation not started") | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | s.tracker.EmitPhaseStarted("Repository Setup", 6, 8) | |
| 217 | + | defer s.tracker.EmitPhaseCompleted("Repository Setup", 6, 8) | |
| 218 | + | ||
| 219 | + | result, err := s.reposHandler.Handle(ctx, cmd) | |
| 220 | + | if err != nil { | |
| 221 | + | s.tracker.EmitPhaseError("Repository Setup", 6, 8, err.Error()) | |
| 222 | + | return nil, err | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | if !result.Success { | |
| 226 | + | s.tracker.EmitPhaseError("Repository Setup", 6, 8, result.ErrorDetail) | |
| 227 | + | return result, errors.New(result.ErrorDetail) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | return result, nil | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | // RunPostInstall runs post-installation tasks | |
| 234 | + | func (s *InstallationService) RunPostInstall(ctx context.Context, cmd commands.PostInstallCommand) (*dto.PostInstallResult, error) { | |
| 235 | + | if s.installAgg == nil { | |
| 236 | + | return nil, errors.New("installation not started") | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | s.tracker.EmitPhaseStarted("Post-Installation", 7, 8) | |
| 240 | + | defer s.tracker.EmitPhaseCompleted("Post-Installation", 7, 8) | |
| 241 | + | ||
| 242 | + | result, err := s.postInstallHandler.Handle(ctx, cmd) | |
| 243 | + | if err != nil { | |
| 244 | + | s.tracker.EmitPhaseError("Post-Installation", 7, 8, err.Error()) | |
| 245 | + | return nil, err | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | if !result.Success { | |
| 249 | + | s.tracker.EmitPhaseError("Post-Installation", 7, 8, result.ErrorDetail) | |
| 250 | + | return result, errors.New(result.ErrorDetail) | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | return result, nil | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | // Complete marks installation as complete | |
| 257 | + | func (s *InstallationService) Complete(ctx context.Context) error { | |
| 258 | + | if s.installAgg == nil { | |
| 259 | + | return errors.New("installation not started") | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | duration := int(time.Since(s.startTime).Seconds()) | |
| 263 | + | if err := s.installAgg.Complete(duration); err != nil { | |
| 264 | + | s.logger.Error("Failed to complete installation", "error", err) | |
| 265 | + | s.tracker.EmitPhaseError("Completion", 8, 8, err.Error()) | |
| 266 | + | return err | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | s.tracker.EmitPhaseCompleted("Completion", 8, 8) | |
| 270 | + | s.logger.Info("Installation completed successfully", "duration", duration) | |
| 271 | + | ||
| 272 | + | return nil | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | // GetStatus returns the current installation status | |
| 276 | + | func (s *InstallationService) GetStatus() *dto.InstallationStatus { | |
| 277 | + | if s.installAgg == nil { | |
| 278 | + | return &dto.InstallationStatus{ | |
| 279 | + | State: "NotStarted", | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | completedAt := s.installAgg.CompletedAt() | |
| 284 | + | startedAt := s.installAgg.StartedAt() | |
| 285 | + | ||
| 286 | + | return &dto.InstallationStatus{ | |
| 287 | + | ID: s.installAgg.ID(), | |
| 288 | + | State: s.installAgg.State().String(), | |
| 289 | + | Hostname: s.installAgg.Hostname(), | |
| 290 | + | Username: s.installAgg.Username(), | |
| 291 | + | TargetDisk: s.installAgg.TargetDisk(), | |
| 292 | + | EncryptionType: s.installAgg.EncryptionType(), | |
| 293 | + | Progress: s.installAgg.ProgressPercentage(), | |
| 294 | + | StartedAt: startedAt, | |
| 295 | + | CompletedAt: completedAt, | |
| 296 | + | CurrentPhase: s.installAgg.State().String(), | |
| 297 | + | } | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | // SubscribeProgress returns a channel for progress updates | |
| 301 | + | func (s *InstallationService) SubscribeProgress() <-chan *dto.ProgressUpdate { | |
| 302 | + | return s.tracker.Subscribe() | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // Close closes the service | |
| 306 | + | func (s *InstallationService) Close() error { | |
| 307 | + | s.tracker.Close() | |
| 308 | + | return nil | |
| 309 | + | } | |
| @@ -0,0 +1,309 @@ | |||||
| 1 | + | package services | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 9 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 10 | + | "github.com/bnema/archup/internal/application/handlers" | |||
| 11 | + | "github.com/bnema/archup/internal/domain/installation" | |||
| 12 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // InstallationService orchestrates the entire installation process | |||
| 16 | + | type InstallationService struct { | |||
| 17 | + | // Domain | |||
| 18 | + | installAgg *installation.Installation | |||
| 19 | + | ||||
| 20 | + | // Handlers | |||
| 21 | + | preflightHandler *handlers.PreflightHandler | |||
| 22 | + | partitionHandler *handlers.PartitionHandler | |||
| 23 | + | baseHandler *handlers.InstallBaseHandler | |||
| 24 | + | configHandler *handlers.ConfigureSystemHandler | |||
| 25 | + | bootloaderHandler *handlers.BootloaderHandler | |||
| 26 | + | reposHandler *handlers.ReposHandler | |||
| 27 | + | postInstallHandler *handlers.PostInstallHandler | |||
| 28 | + | ||||
| 29 | + | // Ports | |||
| 30 | + | repo ports.InstallationRepository | |||
| 31 | + | logger ports.Logger | |||
| 32 | + | ||||
| 33 | + | // Progress tracking | |||
| 34 | + | tracker *ProgressTracker | |||
| 35 | + | ||||
| 36 | + | // State | |||
| 37 | + | startTime time.Time | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // NewInstallationService creates a new installation service with all handlers | |||
| 41 | + | func NewInstallationService( | |||
| 42 | + | repo ports.InstallationRepository, | |||
| 43 | + | logger ports.Logger, | |||
| 44 | + | preflightHandler *handlers.PreflightHandler, | |||
| 45 | + | partitionHandler *handlers.PartitionHandler, | |||
| 46 | + | baseHandler *handlers.InstallBaseHandler, | |||
| 47 | + | configHandler *handlers.ConfigureSystemHandler, | |||
| 48 | + | bootloaderHandler *handlers.BootloaderHandler, | |||
| 49 | + | reposHandler *handlers.ReposHandler, | |||
| 50 | + | postInstallHandler *handlers.PostInstallHandler, | |||
| 51 | + | ) *InstallationService { | |||
| 52 | + | return &InstallationService{ | |||
| 53 | + | repo: repo, | |||
| 54 | + | logger: logger, | |||
| 55 | + | preflightHandler: preflightHandler, | |||
| 56 | + | partitionHandler: partitionHandler, | |||
| 57 | + | baseHandler: baseHandler, | |||
| 58 | + | configHandler: configHandler, | |||
| 59 | + | bootloaderHandler: bootloaderHandler, | |||
| 60 | + | reposHandler: reposHandler, | |||
| 61 | + | postInstallHandler: postInstallHandler, | |||
| 62 | + | tracker: NewProgressTracker(), | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Start begins a new installation | |||
| 67 | + | func (s *InstallationService) Start(ctx context.Context, hostname string, username string, targetDisk string, encryptionType string) error { | |||
| 68 | + | s.logger.Info("Initializing installation", "hostname", hostname, "username", username) | |||
| 69 | + | ||||
| 70 | + | // Create installation aggregate | |||
| 71 | + | inst, err := installation.NewInstallation(hostname, username, targetDisk, encryptionType) | |||
| 72 | + | if err != nil { | |||
| 73 | + | s.logger.Error("Failed to create installation", "error", err) | |||
| 74 | + | s.tracker.EmitPhaseError("initialization", 0, 8, err.Error()) | |||
| 75 | + | return err | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | s.installAgg = inst | |||
| 79 | + | s.startTime = time.Now() | |||
| 80 | + | ||||
| 81 | + | // Start the installation | |||
| 82 | + | if err := s.installAgg.Start(ctx); err != nil { | |||
| 83 | + | s.logger.Error("Failed to start installation", "error", err) | |||
| 84 | + | s.tracker.EmitPhaseError("initialization", 0, 8, err.Error()) | |||
| 85 | + | return err | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | return nil | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // RunPreflight runs preflight checks | |||
| 92 | + | func (s *InstallationService) RunPreflight(ctx context.Context) (*dto.PreflightResult, error) { | |||
| 93 | + | if s.installAgg == nil { | |||
| 94 | + | return nil, errors.New("installation not started") | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | s.tracker.EmitPhaseStarted("Preflight Checks", 1, 8) | |||
| 98 | + | defer s.tracker.EmitPhaseCompleted("Preflight Checks", 1, 8) | |||
| 99 | + | ||||
| 100 | + | result, err := s.preflightHandler.Handle(ctx, commands.PreflightCommand{}) | |||
| 101 | + | if err != nil { | |||
| 102 | + | s.tracker.EmitPhaseError("Preflight Checks", 1, 8, err.Error()) | |||
| 103 | + | return nil, err | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | if !result.ChecksPassed { | |||
| 107 | + | errMsg := "Preflight checks failed" | |||
| 108 | + | if len(result.CriticalErrors) > 0 { | |||
| 109 | + | errMsg = result.CriticalErrors[0] | |||
| 110 | + | } | |||
| 111 | + | s.tracker.EmitPhaseError("Preflight Checks", 1, 8, errMsg) | |||
| 112 | + | return result, errors.New(errMsg) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | return result, nil | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // RunPartition runs disk partitioning | |||
| 119 | + | func (s *InstallationService) RunPartition(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) { | |||
| 120 | + | if s.installAgg == nil { | |||
| 121 | + | return nil, errors.New("installation not started") | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | s.tracker.EmitPhaseStarted("Disk Partitioning", 2, 8) | |||
| 125 | + | defer s.tracker.EmitPhaseCompleted("Disk Partitioning", 2, 8) | |||
| 126 | + | ||||
| 127 | + | result, err := s.partitionHandler.Handle(ctx, cmd) | |||
| 128 | + | if err != nil { | |||
| 129 | + | s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, err.Error()) | |||
| 130 | + | return nil, err | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | if !result.Success { | |||
| 134 | + | s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, result.ErrorDetail) | |||
| 135 | + | return result, errors.New(result.ErrorDetail) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | return result, nil | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // RunBaseInstall runs base system installation | |||
| 142 | + | func (s *InstallationService) RunBaseInstall(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) { | |||
| 143 | + | if s.installAgg == nil { | |||
| 144 | + | return nil, errors.New("installation not started") | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | s.tracker.EmitPhaseStarted("Base Installation", 3, 8) | |||
| 148 | + | defer s.tracker.EmitPhaseCompleted("Base Installation", 3, 8) | |||
| 149 | + | ||||
| 150 | + | result, err := s.baseHandler.Handle(ctx, cmd) | |||
| 151 | + | if err != nil { | |||
| 152 | + | s.tracker.EmitPhaseError("Base Installation", 3, 8, err.Error()) | |||
| 153 | + | return nil, err | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | if !result.Success { | |||
| 157 | + | s.tracker.EmitPhaseError("Base Installation", 3, 8, result.ErrorDetail) | |||
| 158 | + | return result, errors.New(result.ErrorDetail) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | return result, nil | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // RunConfigSystem runs system configuration | |||
| 165 | + | func (s *InstallationService) RunConfigSystem(ctx context.Context, cmd commands.ConfigureSystemCommand) (*dto.ConfigureSystemResult, error) { | |||
| 166 | + | if s.installAgg == nil { | |||
| 167 | + | return nil, errors.New("installation not started") | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | s.tracker.EmitPhaseStarted("System Configuration", 4, 8) | |||
| 171 | + | defer s.tracker.EmitPhaseCompleted("System Configuration", 4, 8) | |||
| 172 | + | ||||
| 173 | + | result, err := s.configHandler.Handle(ctx, cmd) | |||
| 174 | + | if err != nil { | |||
| 175 | + | s.tracker.EmitPhaseError("System Configuration", 4, 8, err.Error()) | |||
| 176 | + | return nil, err | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | if !result.Success { | |||
| 180 | + | s.tracker.EmitPhaseError("System Configuration", 4, 8, result.ErrorDetail) | |||
| 181 | + | return result, errors.New(result.ErrorDetail) | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | return result, nil | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // RunBootloaderSetup runs bootloader setup | |||
| 188 | + | func (s *InstallationService) RunBootloaderSetup(ctx context.Context, cmd commands.InstallBootloaderCommand) (*dto.BootloaderResult, error) { | |||
| 189 | + | if s.installAgg == nil { | |||
| 190 | + | return nil, errors.New("installation not started") | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | s.tracker.EmitPhaseStarted("Bootloader Setup", 5, 8) | |||
| 194 | + | defer s.tracker.EmitPhaseCompleted("Bootloader Setup", 5, 8) | |||
| 195 | + | ||||
| 196 | + | result, err := s.bootloaderHandler.Handle(ctx, cmd) | |||
| 197 | + | if err != nil { | |||
| 198 | + | s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, err.Error()) | |||
| 199 | + | return nil, err | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | if !result.Success { | |||
| 203 | + | s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, result.ErrorDetail) | |||
| 204 | + | return result, errors.New(result.ErrorDetail) | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | return result, nil | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // RunRepositorySetup runs repository setup | |||
| 211 | + | func (s *InstallationService) RunRepositorySetup(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) { | |||
| 212 | + | if s.installAgg == nil { | |||
| 213 | + | return nil, errors.New("installation not started") | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | s.tracker.EmitPhaseStarted("Repository Setup", 6, 8) | |||
| 217 | + | defer s.tracker.EmitPhaseCompleted("Repository Setup", 6, 8) | |||
| 218 | + | ||||
| 219 | + | result, err := s.reposHandler.Handle(ctx, cmd) | |||
| 220 | + | if err != nil { | |||
| 221 | + | s.tracker.EmitPhaseError("Repository Setup", 6, 8, err.Error()) | |||
| 222 | + | return nil, err | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | if !result.Success { | |||
| 226 | + | s.tracker.EmitPhaseError("Repository Setup", 6, 8, result.ErrorDetail) | |||
| 227 | + | return result, errors.New(result.ErrorDetail) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | return result, nil | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | // RunPostInstall runs post-installation tasks | |||
| 234 | + | func (s *InstallationService) RunPostInstall(ctx context.Context, cmd commands.PostInstallCommand) (*dto.PostInstallResult, error) { | |||
| 235 | + | if s.installAgg == nil { | |||
| 236 | + | return nil, errors.New("installation not started") | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | s.tracker.EmitPhaseStarted("Post-Installation", 7, 8) | |||
| 240 | + | defer s.tracker.EmitPhaseCompleted("Post-Installation", 7, 8) | |||
| 241 | + | ||||
| 242 | + | result, err := s.postInstallHandler.Handle(ctx, cmd) | |||
| 243 | + | if err != nil { | |||
| 244 | + | s.tracker.EmitPhaseError("Post-Installation", 7, 8, err.Error()) | |||
| 245 | + | return nil, err | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | if !result.Success { | |||
| 249 | + | s.tracker.EmitPhaseError("Post-Installation", 7, 8, result.ErrorDetail) | |||
| 250 | + | return result, errors.New(result.ErrorDetail) | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | return result, nil | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | // Complete marks installation as complete | |||
| 257 | + | func (s *InstallationService) Complete(ctx context.Context) error { | |||
| 258 | + | if s.installAgg == nil { | |||
| 259 | + | return errors.New("installation not started") | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | duration := int(time.Since(s.startTime).Seconds()) | |||
| 263 | + | if err := s.installAgg.Complete(duration); err != nil { | |||
| 264 | + | s.logger.Error("Failed to complete installation", "error", err) | |||
| 265 | + | s.tracker.EmitPhaseError("Completion", 8, 8, err.Error()) | |||
| 266 | + | return err | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | s.tracker.EmitPhaseCompleted("Completion", 8, 8) | |||
| 270 | + | s.logger.Info("Installation completed successfully", "duration", duration) | |||
| 271 | + | ||||
| 272 | + | return nil | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | // GetStatus returns the current installation status | |||
| 276 | + | func (s *InstallationService) GetStatus() *dto.InstallationStatus { | |||
| 277 | + | if s.installAgg == nil { | |||
| 278 | + | return &dto.InstallationStatus{ | |||
| 279 | + | State: "NotStarted", | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | completedAt := s.installAgg.CompletedAt() | |||
| 284 | + | startedAt := s.installAgg.StartedAt() | |||
| 285 | + | ||||
| 286 | + | return &dto.InstallationStatus{ | |||
| 287 | + | ID: s.installAgg.ID(), | |||
| 288 | + | State: s.installAgg.State().String(), | |||
| 289 | + | Hostname: s.installAgg.Hostname(), | |||
| 290 | + | Username: s.installAgg.Username(), | |||
| 291 | + | TargetDisk: s.installAgg.TargetDisk(), | |||
| 292 | + | EncryptionType: s.installAgg.EncryptionType(), | |||
| 293 | + | Progress: s.installAgg.ProgressPercentage(), | |||
| 294 | + | StartedAt: startedAt, | |||
| 295 | + | CompletedAt: completedAt, | |||
| 296 | + | CurrentPhase: s.installAgg.State().String(), | |||
| 297 | + | } | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | // SubscribeProgress returns a channel for progress updates | |||
| 301 | + | func (s *InstallationService) SubscribeProgress() <-chan *dto.ProgressUpdate { | |||
| 302 | + | return s.tracker.Subscribe() | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | // Close closes the service | |||
| 306 | + | func (s *InstallationService) Close() error { | |||
| 307 | + | s.tracker.Close() | |||
| 308 | + | return nil | |||
| 309 | + | } | |||
A internal/application/services/installation_service_test.go +377 -0
| @@ -0,0 +1,377 @@ | |||
| 1 | + | package services | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |
| 8 | + | "github.com/bnema/archup/internal/application/handlers" | |
| 9 | + | "github.com/bnema/archup/internal/domain/bootloader" | |
| 10 | + | "github.com/bnema/archup/internal/domain/disk" | |
| 11 | + | "github.com/bnema/archup/internal/domain/packages" | |
| 12 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 13 | + | "go.uber.org/mock/gomock" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func createTestService(ctrl *gomock.Controller) *InstallationService { | |
| 17 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 18 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 19 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 20 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 21 | + | mockRepo := mocks.NewMockInstallationRepository(ctrl) | |
| 22 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 23 | + | ||
| 24 | + | mockLogger.EXPECT().Info(gomock.Any()).AnyTimes() | |
| 25 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 26 | + | mockLogger.EXPECT().Warn(gomock.Any()).AnyTimes() | |
| 27 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 28 | + | mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() | |
| 29 | + | mockFS.EXPECT().Exists(gomock.Any()).AnyTimes() | |
| 30 | + | ||
| 31 | + | preflightHandler := handlers.NewPreflightHandler(mockFS, mockExec, mockLogger) | |
| 32 | + | partitionHandler := handlers.NewPartitionHandler(mockExec, mockLogger) | |
| 33 | + | baseHandler := handlers.NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |
| 34 | + | configHandler := handlers.NewConfigureSystemHandler(mockChrExec, mockLogger) | |
| 35 | + | bootloaderHandler := handlers.NewBootloaderHandler(mockChrExec, mockLogger) | |
| 36 | + | reposHandler := handlers.NewReposHandler(mockChrExec, mockLogger) | |
| 37 | + | postInstallHandler := handlers.NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |
| 38 | + | ||
| 39 | + | return NewInstallationService( | |
| 40 | + | mockRepo, | |
| 41 | + | mockLogger, | |
| 42 | + | preflightHandler, | |
| 43 | + | partitionHandler, | |
| 44 | + | baseHandler, | |
| 45 | + | configHandler, | |
| 46 | + | bootloaderHandler, | |
| 47 | + | reposHandler, | |
| 48 | + | postInstallHandler, | |
| 49 | + | ) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func TestInstallationService_Start(t *testing.T) { | |
| 53 | + | ctrl := gomock.NewController(t) | |
| 54 | + | defer ctrl.Finish() | |
| 55 | + | ||
| 56 | + | service := createTestService(ctrl) | |
| 57 | + | defer service.Close() | |
| 58 | + | ||
| 59 | + | ctx := context.Background() | |
| 60 | + | err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 61 | + | ||
| 62 | + | if err != nil { | |
| 63 | + | t.Fatalf("expected no error, got %v", err) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | if service.installAgg == nil { | |
| 67 | + | t.Fatal("expected installation to be created") | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | if service.installAgg.Hostname() != "myarch" { | |
| 71 | + | t.Errorf("expected hostname myarch, got %s", service.installAgg.Hostname()) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func TestInstallationService_Start_InvalidHostname(t *testing.T) { | |
| 76 | + | ctrl := gomock.NewController(t) | |
| 77 | + | defer ctrl.Finish() | |
| 78 | + | ||
| 79 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 80 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 81 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 82 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 83 | + | mockRepo := mocks.NewMockInstallationRepository(ctrl) | |
| 84 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 85 | + | ||
| 86 | + | mockLogger.EXPECT().Info(gomock.Any()).AnyTimes() | |
| 87 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 88 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |
| 89 | + | ||
| 90 | + | preflightHandler := handlers.NewPreflightHandler(mockFS, mockExec, mockLogger) | |
| 91 | + | partitionHandler := handlers.NewPartitionHandler(mockExec, mockLogger) | |
| 92 | + | baseHandler := handlers.NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |
| 93 | + | configHandler := handlers.NewConfigureSystemHandler(mockChrExec, mockLogger) | |
| 94 | + | bootloaderHandler := handlers.NewBootloaderHandler(mockChrExec, mockLogger) | |
| 95 | + | reposHandler := handlers.NewReposHandler(mockChrExec, mockLogger) | |
| 96 | + | postInstallHandler := handlers.NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |
| 97 | + | ||
| 98 | + | service := NewInstallationService(mockRepo, mockLogger, preflightHandler, partitionHandler, baseHandler, configHandler, bootloaderHandler, reposHandler, postInstallHandler) | |
| 99 | + | defer service.Close() | |
| 100 | + | ||
| 101 | + | ctx := context.Background() | |
| 102 | + | err := service.Start(ctx, "", "testuser", "/dev/sda", "none") | |
| 103 | + | ||
| 104 | + | if err == nil { | |
| 105 | + | t.Error("expected error for invalid hostname") | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | if service.installAgg != nil { | |
| 109 | + | t.Error("expected installation not to be created") | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func TestInstallationService_GetStatus_NotStarted(t *testing.T) { | |
| 114 | + | ctrl := gomock.NewController(t) | |
| 115 | + | defer ctrl.Finish() | |
| 116 | + | ||
| 117 | + | service := createTestService(ctrl) | |
| 118 | + | defer service.Close() | |
| 119 | + | ||
| 120 | + | status := service.GetStatus() | |
| 121 | + | ||
| 122 | + | if status.State != "NotStarted" { | |
| 123 | + | t.Errorf("expected state NotStarted, got %s", status.State) | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func TestInstallationService_GetStatus_Started(t *testing.T) { | |
| 128 | + | ctrl := gomock.NewController(t) | |
| 129 | + | defer ctrl.Finish() | |
| 130 | + | ||
| 131 | + | service := createTestService(ctrl) | |
| 132 | + | defer service.Close() | |
| 133 | + | ||
| 134 | + | ctx := context.Background() | |
| 135 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 136 | + | ||
| 137 | + | status := service.GetStatus() | |
| 138 | + | ||
| 139 | + | if status.State != "PreflightChecks" { | |
| 140 | + | t.Errorf("expected state PreflightChecks, got %s", status.State) | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | if status.Hostname != "myarch" { | |
| 144 | + | t.Errorf("expected hostname myarch, got %s", status.Hostname) | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | if status.Username != "testuser" { | |
| 148 | + | t.Errorf("expected username testuser, got %s", status.Username) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | func TestInstallationService_RunPreflight_NotRoot(t *testing.T) { | |
| 153 | + | ctrl := gomock.NewController(t) | |
| 154 | + | defer ctrl.Finish() | |
| 155 | + | ||
| 156 | + | service := createTestService(ctrl) | |
| 157 | + | defer service.Close() | |
| 158 | + | ||
| 159 | + | ctx := context.Background() | |
| 160 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 161 | + | ||
| 162 | + | // We're probably not running as root, so preflight will fail | |
| 163 | + | result, _ := service.RunPreflight(ctx) | |
| 164 | + | ||
| 165 | + | // Either expect error or result indicating failure | |
| 166 | + | if result != nil && result.ChecksPassed { | |
| 167 | + | // If we're running as root, both err and checksPassed should be false | |
| 168 | + | t.Error("expected preflight to fail when not running as root") | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func TestInstallationService_RunPartition(t *testing.T) { | |
| 173 | + | ctrl := gomock.NewController(t) | |
| 174 | + | defer ctrl.Finish() | |
| 175 | + | ||
| 176 | + | service := createTestService(ctrl) | |
| 177 | + | defer service.Close() | |
| 178 | + | ||
| 179 | + | ctx := context.Background() | |
| 180 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 181 | + | ||
| 182 | + | cmd := commands.PartitionDiskCommand{ | |
| 183 | + | TargetDisk: "/dev/sda", | |
| 184 | + | RootSizeGB: 50, | |
| 185 | + | BootSizeGB: 1, | |
| 186 | + | EncryptionType: disk.EncryptionTypeNone, | |
| 187 | + | FilesystemType: disk.FilesystemExt4, | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | result, err := service.RunPartition(ctx, cmd) | |
| 191 | + | ||
| 192 | + | if err != nil { | |
| 193 | + | t.Fatalf("expected no error, got %v", err) | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | if !result.Success { | |
| 197 | + | t.Errorf("expected success, got error: %s", result.ErrorDetail) | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | func TestInstallationService_RunBaseInstall(t *testing.T) { | |
| 202 | + | ctrl := gomock.NewController(t) | |
| 203 | + | defer ctrl.Finish() | |
| 204 | + | ||
| 205 | + | service := createTestService(ctrl) | |
| 206 | + | defer service.Close() | |
| 207 | + | ||
| 208 | + | ctx := context.Background() | |
| 209 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 210 | + | ||
| 211 | + | cmd := commands.InstallBaseCommand{ | |
| 212 | + | TargetDisk: "/dev/sda", | |
| 213 | + | MountPoint: "/mnt", | |
| 214 | + | Packages: []string{}, | |
| 215 | + | KernelVariant: packages.KernelStable, | |
| 216 | + | IncludeMicrocode: false, | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | result, err := service.RunBaseInstall(ctx, cmd) | |
| 220 | + | ||
| 221 | + | if err != nil { | |
| 222 | + | t.Fatalf("expected no error, got %v", err) | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | if !result.Success { | |
| 226 | + | t.Error("expected success") | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func TestInstallationService_RunConfigSystem(t *testing.T) { | |
| 231 | + | ctrl := gomock.NewController(t) | |
| 232 | + | defer ctrl.Finish() | |
| 233 | + | ||
| 234 | + | service := createTestService(ctrl) | |
| 235 | + | defer service.Close() | |
| 236 | + | ||
| 237 | + | ctx := context.Background() | |
| 238 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 239 | + | ||
| 240 | + | cmd := commands.ConfigureSystemCommand{ | |
| 241 | + | MountPoint: "/mnt", | |
| 242 | + | Hostname: "myarch", | |
| 243 | + | Timezone: "UTC", | |
| 244 | + | Locale: "en_US.UTF-8", | |
| 245 | + | Keymap: "us", | |
| 246 | + | Username: "testuser", | |
| 247 | + | UserShell: "/bin/bash", | |
| 248 | + | UserPassword: "testpass123", | |
| 249 | + | RootPassword: "rootpass456", | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | result, err := service.RunConfigSystem(ctx, cmd) | |
| 253 | + | ||
| 254 | + | if err != nil { | |
| 255 | + | t.Fatalf("expected no error, got %v", err) | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | if !result.Success { | |
| 259 | + | t.Error("expected success") | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | func TestInstallationService_RunBootloaderSetup(t *testing.T) { | |
| 264 | + | ctrl := gomock.NewController(t) | |
| 265 | + | defer ctrl.Finish() | |
| 266 | + | ||
| 267 | + | service := createTestService(ctrl) | |
| 268 | + | defer service.Close() | |
| 269 | + | ||
| 270 | + | ctx := context.Background() | |
| 271 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 272 | + | ||
| 273 | + | cmd := commands.InstallBootloaderCommand{ | |
| 274 | + | MountPoint: "/mnt", | |
| 275 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |
| 276 | + | TimeoutSeconds: 5, | |
| 277 | + | Branding: "ArchUp", | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | result, err := service.RunBootloaderSetup(ctx, cmd) | |
| 281 | + | ||
| 282 | + | if err != nil { | |
| 283 | + | t.Fatalf("expected no error, got %v", err) | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | if !result.Success { | |
| 287 | + | t.Error("expected success") | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | func TestInstallationService_RunRepositorySetup(t *testing.T) { | |
| 292 | + | ctrl := gomock.NewController(t) | |
| 293 | + | defer ctrl.Finish() | |
| 294 | + | ||
| 295 | + | service := createTestService(ctrl) | |
| 296 | + | defer service.Close() | |
| 297 | + | ||
| 298 | + | ctx := context.Background() | |
| 299 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 300 | + | ||
| 301 | + | cmd := commands.SetupRepositoriesCommand{ | |
| 302 | + | MountPoint: "/mnt", | |
| 303 | + | EnableMultilib: false, | |
| 304 | + | EnableChaotic: false, | |
| 305 | + | AURHelper: packages.AURHelperParu, | |
| 306 | + | AdditionalRepos: []string{}, | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | result, err := service.RunRepositorySetup(ctx, cmd) | |
| 310 | + | ||
| 311 | + | if err != nil { | |
| 312 | + | t.Fatalf("expected no error, got %v", err) | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | if !result.Success { | |
| 316 | + | t.Error("expected success") | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | func TestInstallationService_RunPostInstall(t *testing.T) { | |
| 321 | + | ctrl := gomock.NewController(t) | |
| 322 | + | defer ctrl.Finish() | |
| 323 | + | ||
| 324 | + | service := createTestService(ctrl) | |
| 325 | + | defer service.Close() | |
| 326 | + | ||
| 327 | + | ctx := context.Background() | |
| 328 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |
| 329 | + | ||
| 330 | + | cmd := commands.PostInstallCommand{ | |
| 331 | + | MountPoint: "/mnt", | |
| 332 | + | Username: "testuser", | |
| 333 | + | RunPostBootScripts: false, | |
| 334 | + | PlymouthTheme: "", | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | result, err := service.RunPostInstall(ctx, cmd) | |
| 338 | + | ||
| 339 | + | if err != nil { | |
| 340 | + | t.Fatalf("expected no error, got %v", err) | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | if !result.Success { | |
| 344 | + | t.Error("expected success") | |
| 345 | + | } | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | func TestInstallationService_CompleteNotStarted(t *testing.T) { | |
| 349 | + | ctrl := gomock.NewController(t) | |
| 350 | + | defer ctrl.Finish() | |
| 351 | + | ||
| 352 | + | service := createTestService(ctrl) | |
| 353 | + | defer service.Close() | |
| 354 | + | ||
| 355 | + | ctx := context.Background() | |
| 356 | + | // Don't start installation, try to complete it | |
| 357 | + | ||
| 358 | + | err := service.Complete(ctx) | |
| 359 | + | ||
| 360 | + | if err == nil { | |
| 361 | + | t.Error("expected error when completing unstarted installation") | |
| 362 | + | } | |
| 363 | + | } | |
| 364 | + | ||
| 365 | + | func TestInstallationService_SubscribeProgress(t *testing.T) { | |
| 366 | + | ctrl := gomock.NewController(t) | |
| 367 | + | defer ctrl.Finish() | |
| 368 | + | ||
| 369 | + | service := createTestService(ctrl) | |
| 370 | + | defer service.Close() | |
| 371 | + | ||
| 372 | + | ch := service.SubscribeProgress() | |
| 373 | + | ||
| 374 | + | if ch == nil { | |
| 375 | + | t.Fatal("expected non-nil progress channel") | |
| 376 | + | } | |
| 377 | + | } | |
| @@ -0,0 +1,377 @@ | |||||
| 1 | + | package services | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/commands" | |||
| 8 | + | "github.com/bnema/archup/internal/application/handlers" | |||
| 9 | + | "github.com/bnema/archup/internal/domain/bootloader" | |||
| 10 | + | "github.com/bnema/archup/internal/domain/disk" | |||
| 11 | + | "github.com/bnema/archup/internal/domain/packages" | |||
| 12 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 13 | + | "go.uber.org/mock/gomock" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func createTestService(ctrl *gomock.Controller) *InstallationService { | |||
| 17 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 18 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 19 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 20 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 21 | + | mockRepo := mocks.NewMockInstallationRepository(ctrl) | |||
| 22 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 23 | + | ||||
| 24 | + | mockLogger.EXPECT().Info(gomock.Any()).AnyTimes() | |||
| 25 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 26 | + | mockLogger.EXPECT().Warn(gomock.Any()).AnyTimes() | |||
| 27 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 28 | + | mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() | |||
| 29 | + | mockFS.EXPECT().Exists(gomock.Any()).AnyTimes() | |||
| 30 | + | ||||
| 31 | + | preflightHandler := handlers.NewPreflightHandler(mockFS, mockExec, mockLogger) | |||
| 32 | + | partitionHandler := handlers.NewPartitionHandler(mockExec, mockLogger) | |||
| 33 | + | baseHandler := handlers.NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |||
| 34 | + | configHandler := handlers.NewConfigureSystemHandler(mockChrExec, mockLogger) | |||
| 35 | + | bootloaderHandler := handlers.NewBootloaderHandler(mockChrExec, mockLogger) | |||
| 36 | + | reposHandler := handlers.NewReposHandler(mockChrExec, mockLogger) | |||
| 37 | + | postInstallHandler := handlers.NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |||
| 38 | + | ||||
| 39 | + | return NewInstallationService( | |||
| 40 | + | mockRepo, | |||
| 41 | + | mockLogger, | |||
| 42 | + | preflightHandler, | |||
| 43 | + | partitionHandler, | |||
| 44 | + | baseHandler, | |||
| 45 | + | configHandler, | |||
| 46 | + | bootloaderHandler, | |||
| 47 | + | reposHandler, | |||
| 48 | + | postInstallHandler, | |||
| 49 | + | ) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func TestInstallationService_Start(t *testing.T) { | |||
| 53 | + | ctrl := gomock.NewController(t) | |||
| 54 | + | defer ctrl.Finish() | |||
| 55 | + | ||||
| 56 | + | service := createTestService(ctrl) | |||
| 57 | + | defer service.Close() | |||
| 58 | + | ||||
| 59 | + | ctx := context.Background() | |||
| 60 | + | err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 61 | + | ||||
| 62 | + | if err != nil { | |||
| 63 | + | t.Fatalf("expected no error, got %v", err) | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | if service.installAgg == nil { | |||
| 67 | + | t.Fatal("expected installation to be created") | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | if service.installAgg.Hostname() != "myarch" { | |||
| 71 | + | t.Errorf("expected hostname myarch, got %s", service.installAgg.Hostname()) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func TestInstallationService_Start_InvalidHostname(t *testing.T) { | |||
| 76 | + | ctrl := gomock.NewController(t) | |||
| 77 | + | defer ctrl.Finish() | |||
| 78 | + | ||||
| 79 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 80 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 81 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 82 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 83 | + | mockRepo := mocks.NewMockInstallationRepository(ctrl) | |||
| 84 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 85 | + | ||||
| 86 | + | mockLogger.EXPECT().Info(gomock.Any()).AnyTimes() | |||
| 87 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 88 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) | |||
| 89 | + | ||||
| 90 | + | preflightHandler := handlers.NewPreflightHandler(mockFS, mockExec, mockLogger) | |||
| 91 | + | partitionHandler := handlers.NewPartitionHandler(mockExec, mockLogger) | |||
| 92 | + | baseHandler := handlers.NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger) | |||
| 93 | + | configHandler := handlers.NewConfigureSystemHandler(mockChrExec, mockLogger) | |||
| 94 | + | bootloaderHandler := handlers.NewBootloaderHandler(mockChrExec, mockLogger) | |||
| 95 | + | reposHandler := handlers.NewReposHandler(mockChrExec, mockLogger) | |||
| 96 | + | postInstallHandler := handlers.NewPostInstallHandler(mockChrExec, mockScriptExec, mockLogger) | |||
| 97 | + | ||||
| 98 | + | service := NewInstallationService(mockRepo, mockLogger, preflightHandler, partitionHandler, baseHandler, configHandler, bootloaderHandler, reposHandler, postInstallHandler) | |||
| 99 | + | defer service.Close() | |||
| 100 | + | ||||
| 101 | + | ctx := context.Background() | |||
| 102 | + | err := service.Start(ctx, "", "testuser", "/dev/sda", "none") | |||
| 103 | + | ||||
| 104 | + | if err == nil { | |||
| 105 | + | t.Error("expected error for invalid hostname") | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | if service.installAgg != nil { | |||
| 109 | + | t.Error("expected installation not to be created") | |||
| 110 | + | } | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func TestInstallationService_GetStatus_NotStarted(t *testing.T) { | |||
| 114 | + | ctrl := gomock.NewController(t) | |||
| 115 | + | defer ctrl.Finish() | |||
| 116 | + | ||||
| 117 | + | service := createTestService(ctrl) | |||
| 118 | + | defer service.Close() | |||
| 119 | + | ||||
| 120 | + | status := service.GetStatus() | |||
| 121 | + | ||||
| 122 | + | if status.State != "NotStarted" { | |||
| 123 | + | t.Errorf("expected state NotStarted, got %s", status.State) | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func TestInstallationService_GetStatus_Started(t *testing.T) { | |||
| 128 | + | ctrl := gomock.NewController(t) | |||
| 129 | + | defer ctrl.Finish() | |||
| 130 | + | ||||
| 131 | + | service := createTestService(ctrl) | |||
| 132 | + | defer service.Close() | |||
| 133 | + | ||||
| 134 | + | ctx := context.Background() | |||
| 135 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 136 | + | ||||
| 137 | + | status := service.GetStatus() | |||
| 138 | + | ||||
| 139 | + | if status.State != "PreflightChecks" { | |||
| 140 | + | t.Errorf("expected state PreflightChecks, got %s", status.State) | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | if status.Hostname != "myarch" { | |||
| 144 | + | t.Errorf("expected hostname myarch, got %s", status.Hostname) | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | if status.Username != "testuser" { | |||
| 148 | + | t.Errorf("expected username testuser, got %s", status.Username) | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | func TestInstallationService_RunPreflight_NotRoot(t *testing.T) { | |||
| 153 | + | ctrl := gomock.NewController(t) | |||
| 154 | + | defer ctrl.Finish() | |||
| 155 | + | ||||
| 156 | + | service := createTestService(ctrl) | |||
| 157 | + | defer service.Close() | |||
| 158 | + | ||||
| 159 | + | ctx := context.Background() | |||
| 160 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 161 | + | ||||
| 162 | + | // We're probably not running as root, so preflight will fail | |||
| 163 | + | result, _ := service.RunPreflight(ctx) | |||
| 164 | + | ||||
| 165 | + | // Either expect error or result indicating failure | |||
| 166 | + | if result != nil && result.ChecksPassed { | |||
| 167 | + | // If we're running as root, both err and checksPassed should be false | |||
| 168 | + | t.Error("expected preflight to fail when not running as root") | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | func TestInstallationService_RunPartition(t *testing.T) { | |||
| 173 | + | ctrl := gomock.NewController(t) | |||
| 174 | + | defer ctrl.Finish() | |||
| 175 | + | ||||
| 176 | + | service := createTestService(ctrl) | |||
| 177 | + | defer service.Close() | |||
| 178 | + | ||||
| 179 | + | ctx := context.Background() | |||
| 180 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 181 | + | ||||
| 182 | + | cmd := commands.PartitionDiskCommand{ | |||
| 183 | + | TargetDisk: "/dev/sda", | |||
| 184 | + | RootSizeGB: 50, | |||
| 185 | + | BootSizeGB: 1, | |||
| 186 | + | EncryptionType: disk.EncryptionTypeNone, | |||
| 187 | + | FilesystemType: disk.FilesystemExt4, | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | result, err := service.RunPartition(ctx, cmd) | |||
| 191 | + | ||||
| 192 | + | if err != nil { | |||
| 193 | + | t.Fatalf("expected no error, got %v", err) | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | if !result.Success { | |||
| 197 | + | t.Errorf("expected success, got error: %s", result.ErrorDetail) | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | func TestInstallationService_RunBaseInstall(t *testing.T) { | |||
| 202 | + | ctrl := gomock.NewController(t) | |||
| 203 | + | defer ctrl.Finish() | |||
| 204 | + | ||||
| 205 | + | service := createTestService(ctrl) | |||
| 206 | + | defer service.Close() | |||
| 207 | + | ||||
| 208 | + | ctx := context.Background() | |||
| 209 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 210 | + | ||||
| 211 | + | cmd := commands.InstallBaseCommand{ | |||
| 212 | + | TargetDisk: "/dev/sda", | |||
| 213 | + | MountPoint: "/mnt", | |||
| 214 | + | Packages: []string{}, | |||
| 215 | + | KernelVariant: packages.KernelStable, | |||
| 216 | + | IncludeMicrocode: false, | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | result, err := service.RunBaseInstall(ctx, cmd) | |||
| 220 | + | ||||
| 221 | + | if err != nil { | |||
| 222 | + | t.Fatalf("expected no error, got %v", err) | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | if !result.Success { | |||
| 226 | + | t.Error("expected success") | |||
| 227 | + | } | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | func TestInstallationService_RunConfigSystem(t *testing.T) { | |||
| 231 | + | ctrl := gomock.NewController(t) | |||
| 232 | + | defer ctrl.Finish() | |||
| 233 | + | ||||
| 234 | + | service := createTestService(ctrl) | |||
| 235 | + | defer service.Close() | |||
| 236 | + | ||||
| 237 | + | ctx := context.Background() | |||
| 238 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 239 | + | ||||
| 240 | + | cmd := commands.ConfigureSystemCommand{ | |||
| 241 | + | MountPoint: "/mnt", | |||
| 242 | + | Hostname: "myarch", | |||
| 243 | + | Timezone: "UTC", | |||
| 244 | + | Locale: "en_US.UTF-8", | |||
| 245 | + | Keymap: "us", | |||
| 246 | + | Username: "testuser", | |||
| 247 | + | UserShell: "/bin/bash", | |||
| 248 | + | UserPassword: "testpass123", | |||
| 249 | + | RootPassword: "rootpass456", | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | result, err := service.RunConfigSystem(ctx, cmd) | |||
| 253 | + | ||||
| 254 | + | if err != nil { | |||
| 255 | + | t.Fatalf("expected no error, got %v", err) | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | if !result.Success { | |||
| 259 | + | t.Error("expected success") | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | func TestInstallationService_RunBootloaderSetup(t *testing.T) { | |||
| 264 | + | ctrl := gomock.NewController(t) | |||
| 265 | + | defer ctrl.Finish() | |||
| 266 | + | ||||
| 267 | + | service := createTestService(ctrl) | |||
| 268 | + | defer service.Close() | |||
| 269 | + | ||||
| 270 | + | ctx := context.Background() | |||
| 271 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 272 | + | ||||
| 273 | + | cmd := commands.InstallBootloaderCommand{ | |||
| 274 | + | MountPoint: "/mnt", | |||
| 275 | + | BootloaderType: bootloader.BootloaderTypeLimine, | |||
| 276 | + | TimeoutSeconds: 5, | |||
| 277 | + | Branding: "ArchUp", | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | result, err := service.RunBootloaderSetup(ctx, cmd) | |||
| 281 | + | ||||
| 282 | + | if err != nil { | |||
| 283 | + | t.Fatalf("expected no error, got %v", err) | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | if !result.Success { | |||
| 287 | + | t.Error("expected success") | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | func TestInstallationService_RunRepositorySetup(t *testing.T) { | |||
| 292 | + | ctrl := gomock.NewController(t) | |||
| 293 | + | defer ctrl.Finish() | |||
| 294 | + | ||||
| 295 | + | service := createTestService(ctrl) | |||
| 296 | + | defer service.Close() | |||
| 297 | + | ||||
| 298 | + | ctx := context.Background() | |||
| 299 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 300 | + | ||||
| 301 | + | cmd := commands.SetupRepositoriesCommand{ | |||
| 302 | + | MountPoint: "/mnt", | |||
| 303 | + | EnableMultilib: false, | |||
| 304 | + | EnableChaotic: false, | |||
| 305 | + | AURHelper: packages.AURHelperParu, | |||
| 306 | + | AdditionalRepos: []string{}, | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | result, err := service.RunRepositorySetup(ctx, cmd) | |||
| 310 | + | ||||
| 311 | + | if err != nil { | |||
| 312 | + | t.Fatalf("expected no error, got %v", err) | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | if !result.Success { | |||
| 316 | + | t.Error("expected success") | |||
| 317 | + | } | |||
| 318 | + | } | |||
| 319 | + | ||||
| 320 | + | func TestInstallationService_RunPostInstall(t *testing.T) { | |||
| 321 | + | ctrl := gomock.NewController(t) | |||
| 322 | + | defer ctrl.Finish() | |||
| 323 | + | ||||
| 324 | + | service := createTestService(ctrl) | |||
| 325 | + | defer service.Close() | |||
| 326 | + | ||||
| 327 | + | ctx := context.Background() | |||
| 328 | + | service.Start(ctx, "myarch", "testuser", "/dev/sda", "none") | |||
| 329 | + | ||||
| 330 | + | cmd := commands.PostInstallCommand{ | |||
| 331 | + | MountPoint: "/mnt", | |||
| 332 | + | Username: "testuser", | |||
| 333 | + | RunPostBootScripts: false, | |||
| 334 | + | PlymouthTheme: "", | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | result, err := service.RunPostInstall(ctx, cmd) | |||
| 338 | + | ||||
| 339 | + | if err != nil { | |||
| 340 | + | t.Fatalf("expected no error, got %v", err) | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | if !result.Success { | |||
| 344 | + | t.Error("expected success") | |||
| 345 | + | } | |||
| 346 | + | } | |||
| 347 | + | ||||
| 348 | + | func TestInstallationService_CompleteNotStarted(t *testing.T) { | |||
| 349 | + | ctrl := gomock.NewController(t) | |||
| 350 | + | defer ctrl.Finish() | |||
| 351 | + | ||||
| 352 | + | service := createTestService(ctrl) | |||
| 353 | + | defer service.Close() | |||
| 354 | + | ||||
| 355 | + | ctx := context.Background() | |||
| 356 | + | // Don't start installation, try to complete it | |||
| 357 | + | ||||
| 358 | + | err := service.Complete(ctx) | |||
| 359 | + | ||||
| 360 | + | if err == nil { | |||
| 361 | + | t.Error("expected error when completing unstarted installation") | |||
| 362 | + | } | |||
| 363 | + | } | |||
| 364 | + | ||||
| 365 | + | func TestInstallationService_SubscribeProgress(t *testing.T) { | |||
| 366 | + | ctrl := gomock.NewController(t) | |||
| 367 | + | defer ctrl.Finish() | |||
| 368 | + | ||||
| 369 | + | service := createTestService(ctrl) | |||
| 370 | + | defer service.Close() | |||
| 371 | + | ||||
| 372 | + | ch := service.SubscribeProgress() | |||
| 373 | + | ||||
| 374 | + | if ch == nil { | |||
| 375 | + | t.Fatal("expected non-nil progress channel") | |||
| 376 | + | } | |||
| 377 | + | } | |||
A internal/application/services/progress_tracker.go +110 -0
| @@ -0,0 +1,110 @@ | |||
| 1 | + | package services | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/dto" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // ProgressTracker tracks and broadcasts installation progress | |
| 11 | + | type ProgressTracker struct { | |
| 12 | + | subscribers []chan *dto.ProgressUpdate | |
| 13 | + | mu sync.RWMutex | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // NewProgressTracker creates a new progress tracker | |
| 17 | + | func NewProgressTracker() *ProgressTracker { | |
| 18 | + | return &ProgressTracker{ | |
| 19 | + | subscribers: make([]chan *dto.ProgressUpdate, 0), | |
| 20 | + | } | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // Subscribe returns a channel for progress updates | |
| 24 | + | func (pt *ProgressTracker) Subscribe() <-chan *dto.ProgressUpdate { | |
| 25 | + | pt.mu.Lock() | |
| 26 | + | defer pt.mu.Unlock() | |
| 27 | + | ||
| 28 | + | ch := make(chan *dto.ProgressUpdate, 10) | |
| 29 | + | pt.subscribers = append(pt.subscribers, ch) | |
| 30 | + | return ch | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // Unsubscribe removes a subscriber | |
| 34 | + | func (pt *ProgressTracker) Unsubscribe(ch chan *dto.ProgressUpdate) { | |
| 35 | + | pt.mu.Lock() | |
| 36 | + | defer pt.mu.Unlock() | |
| 37 | + | ||
| 38 | + | for i, sub := range pt.subscribers { | |
| 39 | + | if sub == ch { | |
| 40 | + | pt.subscribers = append(pt.subscribers[:i], pt.subscribers[i+1:]...) | |
| 41 | + | close(ch) | |
| 42 | + | break | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Emit sends a progress update to all subscribers | |
| 48 | + | func (pt *ProgressTracker) Emit(update *dto.ProgressUpdate) { | |
| 49 | + | pt.mu.RLock() | |
| 50 | + | defer pt.mu.RUnlock() | |
| 51 | + | ||
| 52 | + | if update.Timestamp.IsZero() { | |
| 53 | + | update.Timestamp = time.Now() | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | for _, ch := range pt.subscribers { | |
| 57 | + | select { | |
| 58 | + | case ch <- update: | |
| 59 | + | default: | |
| 60 | + | // Channel full, skip to avoid blocking | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // EmitPhaseStarted sends a phase started event | |
| 66 | + | func (pt *ProgressTracker) EmitPhaseStarted(phase string, phaseNumber int, totalPhases int) { | |
| 67 | + | pt.Emit(&dto.ProgressUpdate{ | |
| 68 | + | Phase: phase, | |
| 69 | + | PhaseNumber: phaseNumber, | |
| 70 | + | TotalPhases: totalPhases, | |
| 71 | + | ProgressPercent: (phaseNumber - 1) * 100 / totalPhases, | |
| 72 | + | Message: "Starting " + phase, | |
| 73 | + | IsError: false, | |
| 74 | + | }) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // EmitPhaseCompleted sends a phase completed event | |
| 78 | + | func (pt *ProgressTracker) EmitPhaseCompleted(phase string, phaseNumber int, totalPhases int) { | |
| 79 | + | pt.Emit(&dto.ProgressUpdate{ | |
| 80 | + | Phase: phase, | |
| 81 | + | PhaseNumber: phaseNumber, | |
| 82 | + | TotalPhases: totalPhases, | |
| 83 | + | ProgressPercent: phaseNumber * 100 / totalPhases, | |
| 84 | + | Message: phase + " completed", | |
| 85 | + | IsError: false, | |
| 86 | + | }) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // EmitPhaseError sends a phase error event | |
| 90 | + | func (pt *ProgressTracker) EmitPhaseError(phase string, phaseNumber int, totalPhases int, errorMsg string) { | |
| 91 | + | pt.Emit(&dto.ProgressUpdate{ | |
| 92 | + | Phase: phase, | |
| 93 | + | PhaseNumber: phaseNumber, | |
| 94 | + | TotalPhases: totalPhases, | |
| 95 | + | ProgressPercent: (phaseNumber - 1) * 100 / totalPhases, | |
| 96 | + | Message: errorMsg, | |
| 97 | + | IsError: true, | |
| 98 | + | }) | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // Close closes all subscriber channels | |
| 102 | + | func (pt *ProgressTracker) Close() { | |
| 103 | + | pt.mu.Lock() | |
| 104 | + | defer pt.mu.Unlock() | |
| 105 | + | ||
| 106 | + | for _, ch := range pt.subscribers { | |
| 107 | + | close(ch) | |
| 108 | + | } | |
| 109 | + | pt.subscribers = nil | |
| 110 | + | } | |
| @@ -0,0 +1,110 @@ | |||||
| 1 | + | package services | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // ProgressTracker tracks and broadcasts installation progress | |||
| 11 | + | type ProgressTracker struct { | |||
| 12 | + | subscribers []chan *dto.ProgressUpdate | |||
| 13 | + | mu sync.RWMutex | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // NewProgressTracker creates a new progress tracker | |||
| 17 | + | func NewProgressTracker() *ProgressTracker { | |||
| 18 | + | return &ProgressTracker{ | |||
| 19 | + | subscribers: make([]chan *dto.ProgressUpdate, 0), | |||
| 20 | + | } | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // Subscribe returns a channel for progress updates | |||
| 24 | + | func (pt *ProgressTracker) Subscribe() <-chan *dto.ProgressUpdate { | |||
| 25 | + | pt.mu.Lock() | |||
| 26 | + | defer pt.mu.Unlock() | |||
| 27 | + | ||||
| 28 | + | ch := make(chan *dto.ProgressUpdate, 10) | |||
| 29 | + | pt.subscribers = append(pt.subscribers, ch) | |||
| 30 | + | return ch | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // Unsubscribe removes a subscriber | |||
| 34 | + | func (pt *ProgressTracker) Unsubscribe(ch chan *dto.ProgressUpdate) { | |||
| 35 | + | pt.mu.Lock() | |||
| 36 | + | defer pt.mu.Unlock() | |||
| 37 | + | ||||
| 38 | + | for i, sub := range pt.subscribers { | |||
| 39 | + | if sub == ch { | |||
| 40 | + | pt.subscribers = append(pt.subscribers[:i], pt.subscribers[i+1:]...) | |||
| 41 | + | close(ch) | |||
| 42 | + | break | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Emit sends a progress update to all subscribers | |||
| 48 | + | func (pt *ProgressTracker) Emit(update *dto.ProgressUpdate) { | |||
| 49 | + | pt.mu.RLock() | |||
| 50 | + | defer pt.mu.RUnlock() | |||
| 51 | + | ||||
| 52 | + | if update.Timestamp.IsZero() { | |||
| 53 | + | update.Timestamp = time.Now() | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | for _, ch := range pt.subscribers { | |||
| 57 | + | select { | |||
| 58 | + | case ch <- update: | |||
| 59 | + | default: | |||
| 60 | + | // Channel full, skip to avoid blocking | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // EmitPhaseStarted sends a phase started event | |||
| 66 | + | func (pt *ProgressTracker) EmitPhaseStarted(phase string, phaseNumber int, totalPhases int) { | |||
| 67 | + | pt.Emit(&dto.ProgressUpdate{ | |||
| 68 | + | Phase: phase, | |||
| 69 | + | PhaseNumber: phaseNumber, | |||
| 70 | + | TotalPhases: totalPhases, | |||
| 71 | + | ProgressPercent: (phaseNumber - 1) * 100 / totalPhases, | |||
| 72 | + | Message: "Starting " + phase, | |||
| 73 | + | IsError: false, | |||
| 74 | + | }) | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // EmitPhaseCompleted sends a phase completed event | |||
| 78 | + | func (pt *ProgressTracker) EmitPhaseCompleted(phase string, phaseNumber int, totalPhases int) { | |||
| 79 | + | pt.Emit(&dto.ProgressUpdate{ | |||
| 80 | + | Phase: phase, | |||
| 81 | + | PhaseNumber: phaseNumber, | |||
| 82 | + | TotalPhases: totalPhases, | |||
| 83 | + | ProgressPercent: phaseNumber * 100 / totalPhases, | |||
| 84 | + | Message: phase + " completed", | |||
| 85 | + | IsError: false, | |||
| 86 | + | }) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // EmitPhaseError sends a phase error event | |||
| 90 | + | func (pt *ProgressTracker) EmitPhaseError(phase string, phaseNumber int, totalPhases int, errorMsg string) { | |||
| 91 | + | pt.Emit(&dto.ProgressUpdate{ | |||
| 92 | + | Phase: phase, | |||
| 93 | + | PhaseNumber: phaseNumber, | |||
| 94 | + | TotalPhases: totalPhases, | |||
| 95 | + | ProgressPercent: (phaseNumber - 1) * 100 / totalPhases, | |||
| 96 | + | Message: errorMsg, | |||
| 97 | + | IsError: true, | |||
| 98 | + | }) | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // Close closes all subscriber channels | |||
| 102 | + | func (pt *ProgressTracker) Close() { | |||
| 103 | + | pt.mu.Lock() | |||
| 104 | + | defer pt.mu.Unlock() | |||
| 105 | + | ||||
| 106 | + | for _, ch := range pt.subscribers { | |||
| 107 | + | close(ch) | |||
| 108 | + | } | |||
| 109 | + | pt.subscribers = nil | |||
| 110 | + | } | |||
A internal/application/services/progress_tracker_test.go +175 -0
| @@ -0,0 +1,175 @@ | |||
| 1 | + | package services | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/application/dto" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestProgressTracker_Subscribe(t *testing.T) { | |
| 11 | + | tracker := NewProgressTracker() | |
| 12 | + | defer tracker.Close() | |
| 13 | + | ||
| 14 | + | ch := tracker.Subscribe() | |
| 15 | + | if ch == nil { | |
| 16 | + | t.Fatal("expected non-nil channel") | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func TestProgressTracker_Emit(t *testing.T) { | |
| 21 | + | tracker := NewProgressTracker() | |
| 22 | + | defer tracker.Close() | |
| 23 | + | ||
| 24 | + | ch := tracker.Subscribe() | |
| 25 | + | ||
| 26 | + | update := &dto.ProgressUpdate{ | |
| 27 | + | Phase: "Test Phase", | |
| 28 | + | PhaseNumber: 1, | |
| 29 | + | TotalPhases: 8, | |
| 30 | + | ProgressPercent: 10, | |
| 31 | + | Message: "Test message", | |
| 32 | + | IsError: false, | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | go tracker.Emit(update) | |
| 36 | + | ||
| 37 | + | select { | |
| 38 | + | case received := <-ch: | |
| 39 | + | if received.Phase != "Test Phase" { | |
| 40 | + | t.Errorf("expected phase 'Test Phase', got %s", received.Phase) | |
| 41 | + | } | |
| 42 | + | if received.Message != "Test message" { | |
| 43 | + | t.Errorf("expected message 'Test message', got %s", received.Message) | |
| 44 | + | } | |
| 45 | + | case <-time.After(1 * time.Second): | |
| 46 | + | t.Fatal("timeout waiting for progress update") | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestProgressTracker_MultipleSubscribers(t *testing.T) { | |
| 51 | + | tracker := NewProgressTracker() | |
| 52 | + | defer tracker.Close() | |
| 53 | + | ||
| 54 | + | ch1 := tracker.Subscribe() | |
| 55 | + | ch2 := tracker.Subscribe() | |
| 56 | + | ||
| 57 | + | update := &dto.ProgressUpdate{ | |
| 58 | + | Phase: "Test", | |
| 59 | + | Message: "Test message", | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | go tracker.Emit(update) | |
| 63 | + | ||
| 64 | + | // Both channels should receive the update | |
| 65 | + | select { | |
| 66 | + | case <-ch1: | |
| 67 | + | case <-time.After(1 * time.Second): | |
| 68 | + | t.Fatal("timeout on ch1") | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | select { | |
| 72 | + | case <-ch2: | |
| 73 | + | case <-time.After(1 * time.Second): | |
| 74 | + | t.Fatal("timeout on ch2") | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestProgressTracker_EmitPhaseStarted(t *testing.T) { | |
| 79 | + | tracker := NewProgressTracker() | |
| 80 | + | defer tracker.Close() | |
| 81 | + | ||
| 82 | + | ch := tracker.Subscribe() | |
| 83 | + | ||
| 84 | + | go tracker.EmitPhaseStarted("Preflight", 1, 8) | |
| 85 | + | ||
| 86 | + | select { | |
| 87 | + | case update := <-ch: | |
| 88 | + | if update.Phase != "Preflight" { | |
| 89 | + | t.Errorf("expected phase 'Preflight', got %s", update.Phase) | |
| 90 | + | } | |
| 91 | + | if update.Message != "Starting Preflight" { | |
| 92 | + | t.Errorf("expected message 'Starting Preflight', got %s", update.Message) | |
| 93 | + | } | |
| 94 | + | if update.ProgressPercent != 0 { | |
| 95 | + | t.Errorf("expected progress 0%%, got %d%%", update.ProgressPercent) | |
| 96 | + | } | |
| 97 | + | case <-time.After(1 * time.Second): | |
| 98 | + | t.Fatal("timeout waiting for phase started event") | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func TestProgressTracker_EmitPhaseCompleted(t *testing.T) { | |
| 103 | + | tracker := NewProgressTracker() | |
| 104 | + | defer tracker.Close() | |
| 105 | + | ||
| 106 | + | ch := tracker.Subscribe() | |
| 107 | + | ||
| 108 | + | go tracker.EmitPhaseCompleted("Preflight", 1, 8) | |
| 109 | + | ||
| 110 | + | select { | |
| 111 | + | case update := <-ch: | |
| 112 | + | if update.Phase != "Preflight" { | |
| 113 | + | t.Errorf("expected phase 'Preflight', got %s", update.Phase) | |
| 114 | + | } | |
| 115 | + | if update.Message != "Preflight completed" { | |
| 116 | + | t.Errorf("expected message 'Preflight completed', got %s", update.Message) | |
| 117 | + | } | |
| 118 | + | if update.ProgressPercent != 12 { // 1 * 100 / 8 = 12% | |
| 119 | + | t.Errorf("expected progress 12%%, got %d%%", update.ProgressPercent) | |
| 120 | + | } | |
| 121 | + | case <-time.After(1 * time.Second): | |
| 122 | + | t.Fatal("timeout waiting for phase completed event") | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func TestProgressTracker_EmitPhaseError(t *testing.T) { | |
| 127 | + | tracker := NewProgressTracker() | |
| 128 | + | defer tracker.Close() | |
| 129 | + | ||
| 130 | + | ch := tracker.Subscribe() | |
| 131 | + | ||
| 132 | + | go tracker.EmitPhaseError("Preflight", 1, 8, "Something went wrong") | |
| 133 | + | ||
| 134 | + | select { | |
| 135 | + | case update := <-ch: | |
| 136 | + | if update.Phase != "Preflight" { | |
| 137 | + | t.Errorf("expected phase 'Preflight', got %s", update.Phase) | |
| 138 | + | } | |
| 139 | + | if update.Message != "Something went wrong" { | |
| 140 | + | t.Errorf("expected error message, got %s", update.Message) | |
| 141 | + | } | |
| 142 | + | if !update.IsError { | |
| 143 | + | t.Error("expected IsError to be true") | |
| 144 | + | } | |
| 145 | + | case <-time.After(1 * time.Second): | |
| 146 | + | t.Fatal("timeout waiting for phase error event") | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // Note: Unsubscribe test is not practical since Subscribe returns a read-only channel | |
| 151 | + | // and the internal channel is not directly accessible to the caller. | |
| 152 | + | ||
| 153 | + | func TestProgressTracker_TimestampSet(t *testing.T) { | |
| 154 | + | tracker := NewProgressTracker() | |
| 155 | + | defer tracker.Close() | |
| 156 | + | ||
| 157 | + | ch := tracker.Subscribe() | |
| 158 | + | ||
| 159 | + | update := &dto.ProgressUpdate{ | |
| 160 | + | Phase: "Test", | |
| 161 | + | Message: "Test message", | |
| 162 | + | // Timestamp is zero | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | go tracker.Emit(update) | |
| 166 | + | ||
| 167 | + | select { | |
| 168 | + | case received := <-ch: | |
| 169 | + | if received.Timestamp.IsZero() { | |
| 170 | + | t.Error("expected timestamp to be set") | |
| 171 | + | } | |
| 172 | + | case <-time.After(1 * time.Second): | |
| 173 | + | t.Fatal("timeout waiting for progress update") | |
| 174 | + | } | |
| 175 | + | } | |
| @@ -0,0 +1,175 @@ | |||||
| 1 | + | package services | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/application/dto" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestProgressTracker_Subscribe(t *testing.T) { | |||
| 11 | + | tracker := NewProgressTracker() | |||
| 12 | + | defer tracker.Close() | |||
| 13 | + | ||||
| 14 | + | ch := tracker.Subscribe() | |||
| 15 | + | if ch == nil { | |||
| 16 | + | t.Fatal("expected non-nil channel") | |||
| 17 | + | } | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func TestProgressTracker_Emit(t *testing.T) { | |||
| 21 | + | tracker := NewProgressTracker() | |||
| 22 | + | defer tracker.Close() | |||
| 23 | + | ||||
| 24 | + | ch := tracker.Subscribe() | |||
| 25 | + | ||||
| 26 | + | update := &dto.ProgressUpdate{ | |||
| 27 | + | Phase: "Test Phase", | |||
| 28 | + | PhaseNumber: 1, | |||
| 29 | + | TotalPhases: 8, | |||
| 30 | + | ProgressPercent: 10, | |||
| 31 | + | Message: "Test message", | |||
| 32 | + | IsError: false, | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | go tracker.Emit(update) | |||
| 36 | + | ||||
| 37 | + | select { | |||
| 38 | + | case received := <-ch: | |||
| 39 | + | if received.Phase != "Test Phase" { | |||
| 40 | + | t.Errorf("expected phase 'Test Phase', got %s", received.Phase) | |||
| 41 | + | } | |||
| 42 | + | if received.Message != "Test message" { | |||
| 43 | + | t.Errorf("expected message 'Test message', got %s", received.Message) | |||
| 44 | + | } | |||
| 45 | + | case <-time.After(1 * time.Second): | |||
| 46 | + | t.Fatal("timeout waiting for progress update") | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestProgressTracker_MultipleSubscribers(t *testing.T) { | |||
| 51 | + | tracker := NewProgressTracker() | |||
| 52 | + | defer tracker.Close() | |||
| 53 | + | ||||
| 54 | + | ch1 := tracker.Subscribe() | |||
| 55 | + | ch2 := tracker.Subscribe() | |||
| 56 | + | ||||
| 57 | + | update := &dto.ProgressUpdate{ | |||
| 58 | + | Phase: "Test", | |||
| 59 | + | Message: "Test message", | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | go tracker.Emit(update) | |||
| 63 | + | ||||
| 64 | + | // Both channels should receive the update | |||
| 65 | + | select { | |||
| 66 | + | case <-ch1: | |||
| 67 | + | case <-time.After(1 * time.Second): | |||
| 68 | + | t.Fatal("timeout on ch1") | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | select { | |||
| 72 | + | case <-ch2: | |||
| 73 | + | case <-time.After(1 * time.Second): | |||
| 74 | + | t.Fatal("timeout on ch2") | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestProgressTracker_EmitPhaseStarted(t *testing.T) { | |||
| 79 | + | tracker := NewProgressTracker() | |||
| 80 | + | defer tracker.Close() | |||
| 81 | + | ||||
| 82 | + | ch := tracker.Subscribe() | |||
| 83 | + | ||||
| 84 | + | go tracker.EmitPhaseStarted("Preflight", 1, 8) | |||
| 85 | + | ||||
| 86 | + | select { | |||
| 87 | + | case update := <-ch: | |||
| 88 | + | if update.Phase != "Preflight" { | |||
| 89 | + | t.Errorf("expected phase 'Preflight', got %s", update.Phase) | |||
| 90 | + | } | |||
| 91 | + | if update.Message != "Starting Preflight" { | |||
| 92 | + | t.Errorf("expected message 'Starting Preflight', got %s", update.Message) | |||
| 93 | + | } | |||
| 94 | + | if update.ProgressPercent != 0 { | |||
| 95 | + | t.Errorf("expected progress 0%%, got %d%%", update.ProgressPercent) | |||
| 96 | + | } | |||
| 97 | + | case <-time.After(1 * time.Second): | |||
| 98 | + | t.Fatal("timeout waiting for phase started event") | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func TestProgressTracker_EmitPhaseCompleted(t *testing.T) { | |||
| 103 | + | tracker := NewProgressTracker() | |||
| 104 | + | defer tracker.Close() | |||
| 105 | + | ||||
| 106 | + | ch := tracker.Subscribe() | |||
| 107 | + | ||||
| 108 | + | go tracker.EmitPhaseCompleted("Preflight", 1, 8) | |||
| 109 | + | ||||
| 110 | + | select { | |||
| 111 | + | case update := <-ch: | |||
| 112 | + | if update.Phase != "Preflight" { | |||
| 113 | + | t.Errorf("expected phase 'Preflight', got %s", update.Phase) | |||
| 114 | + | } | |||
| 115 | + | if update.Message != "Preflight completed" { | |||
| 116 | + | t.Errorf("expected message 'Preflight completed', got %s", update.Message) | |||
| 117 | + | } | |||
| 118 | + | if update.ProgressPercent != 12 { // 1 * 100 / 8 = 12% | |||
| 119 | + | t.Errorf("expected progress 12%%, got %d%%", update.ProgressPercent) | |||
| 120 | + | } | |||
| 121 | + | case <-time.After(1 * time.Second): | |||
| 122 | + | t.Fatal("timeout waiting for phase completed event") | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func TestProgressTracker_EmitPhaseError(t *testing.T) { | |||
| 127 | + | tracker := NewProgressTracker() | |||
| 128 | + | defer tracker.Close() | |||
| 129 | + | ||||
| 130 | + | ch := tracker.Subscribe() | |||
| 131 | + | ||||
| 132 | + | go tracker.EmitPhaseError("Preflight", 1, 8, "Something went wrong") | |||
| 133 | + | ||||
| 134 | + | select { | |||
| 135 | + | case update := <-ch: | |||
| 136 | + | if update.Phase != "Preflight" { | |||
| 137 | + | t.Errorf("expected phase 'Preflight', got %s", update.Phase) | |||
| 138 | + | } | |||
| 139 | + | if update.Message != "Something went wrong" { | |||
| 140 | + | t.Errorf("expected error message, got %s", update.Message) | |||
| 141 | + | } | |||
| 142 | + | if !update.IsError { | |||
| 143 | + | t.Error("expected IsError to be true") | |||
| 144 | + | } | |||
| 145 | + | case <-time.After(1 * time.Second): | |||
| 146 | + | t.Fatal("timeout waiting for phase error event") | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // Note: Unsubscribe test is not practical since Subscribe returns a read-only channel | |||
| 151 | + | // and the internal channel is not directly accessible to the caller. | |||
| 152 | + | ||||
| 153 | + | func TestProgressTracker_TimestampSet(t *testing.T) { | |||
| 154 | + | tracker := NewProgressTracker() | |||
| 155 | + | defer tracker.Close() | |||
| 156 | + | ||||
| 157 | + | ch := tracker.Subscribe() | |||
| 158 | + | ||||
| 159 | + | update := &dto.ProgressUpdate{ | |||
| 160 | + | Phase: "Test", | |||
| 161 | + | Message: "Test message", | |||
| 162 | + | // Timestamp is zero | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | go tracker.Emit(update) | |||
| 166 | + | ||||
| 167 | + | select { | |||
| 168 | + | case received := <-ch: | |||
| 169 | + | if received.Timestamp.IsZero() { | |||
| 170 | + | t.Error("expected timestamp to be set") | |||
| 171 | + | } | |||
| 172 | + | case <-time.After(1 * time.Second): | |||
| 173 | + | t.Fatal("timeout waiting for progress update") | |||
| 174 | + | } | |||
| 175 | + | } | |||
A internal/domain/ports/mocks/mock_ports.go +686 -0
| @@ -0,0 +1,686 @@ | |||
| 1 | + | // Code generated by MockGen. DO NOT EDIT. | |
| 2 | + | // Source: github.com/bnema/archup/internal/domain/ports (interfaces: FileSystem,File,CommandExecutor,ChrootExecutor,ScriptExecutor,HTTPClient,Response,Logger,InstallationRepository) | |
| 3 | + | // | |
| 4 | + | // Generated by this command: | |
| 5 | + | // | |
| 6 | + | // mockgen -destination=mocks/mock_ports.go -package=mocks . FileSystem,File,CommandExecutor,ChrootExecutor,ScriptExecutor,HTTPClient,Response,Logger,InstallationRepository | |
| 7 | + | // | |
| 8 | + | ||
| 9 | + | // Package mocks is a generated GoMock package. | |
| 10 | + | package mocks | |
| 11 | + | ||
| 12 | + | import ( | |
| 13 | + | context "context" | |
| 14 | + | os "os" | |
| 15 | + | reflect "reflect" | |
| 16 | + | ||
| 17 | + | ports "github.com/bnema/archup/internal/domain/ports" | |
| 18 | + | gomock "go.uber.org/mock/gomock" | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // MockFileSystem is a mock of FileSystem interface. | |
| 22 | + | type MockFileSystem struct { | |
| 23 | + | ctrl *gomock.Controller | |
| 24 | + | recorder *MockFileSystemMockRecorder | |
| 25 | + | isgomock struct{} | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // MockFileSystemMockRecorder is the mock recorder for MockFileSystem. | |
| 29 | + | type MockFileSystemMockRecorder struct { | |
| 30 | + | mock *MockFileSystem | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // NewMockFileSystem creates a new mock instance. | |
| 34 | + | func NewMockFileSystem(ctrl *gomock.Controller) *MockFileSystem { | |
| 35 | + | mock := &MockFileSystem{ctrl: ctrl} | |
| 36 | + | mock.recorder = &MockFileSystemMockRecorder{mock} | |
| 37 | + | return mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 41 | + | func (m *MockFileSystem) EXPECT() *MockFileSystemMockRecorder { | |
| 42 | + | return m.recorder | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // Chmod mocks base method. | |
| 46 | + | func (m *MockFileSystem) Chmod(name string, perm os.FileMode) error { | |
| 47 | + | m.ctrl.T.Helper() | |
| 48 | + | ret := m.ctrl.Call(m, "Chmod", name, perm) | |
| 49 | + | ret0, _ := ret[0].(error) | |
| 50 | + | return ret0 | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Chmod indicates an expected call of Chmod. | |
| 54 | + | func (mr *MockFileSystemMockRecorder) Chmod(name, perm any) *gomock.Call { | |
| 55 | + | mr.mock.ctrl.T.Helper() | |
| 56 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chmod", reflect.TypeOf((*MockFileSystem)(nil).Chmod), name, perm) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Create mocks base method. | |
| 60 | + | func (m *MockFileSystem) Create(name string) (ports.File, error) { | |
| 61 | + | m.ctrl.T.Helper() | |
| 62 | + | ret := m.ctrl.Call(m, "Create", name) | |
| 63 | + | ret0, _ := ret[0].(ports.File) | |
| 64 | + | ret1, _ := ret[1].(error) | |
| 65 | + | return ret0, ret1 | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // Create indicates an expected call of Create. | |
| 69 | + | func (mr *MockFileSystemMockRecorder) Create(name any) *gomock.Call { | |
| 70 | + | mr.mock.ctrl.T.Helper() | |
| 71 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFileSystem)(nil).Create), name) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // Exists mocks base method. | |
| 75 | + | func (m *MockFileSystem) Exists(path string) (bool, error) { | |
| 76 | + | m.ctrl.T.Helper() | |
| 77 | + | ret := m.ctrl.Call(m, "Exists", path) | |
| 78 | + | ret0, _ := ret[0].(bool) | |
| 79 | + | ret1, _ := ret[1].(error) | |
| 80 | + | return ret0, ret1 | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Exists indicates an expected call of Exists. | |
| 84 | + | func (mr *MockFileSystemMockRecorder) Exists(path any) *gomock.Call { | |
| 85 | + | mr.mock.ctrl.T.Helper() | |
| 86 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockFileSystem)(nil).Exists), path) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // MkdirAll mocks base method. | |
| 90 | + | func (m *MockFileSystem) MkdirAll(path string, perm os.FileMode) error { | |
| 91 | + | m.ctrl.T.Helper() | |
| 92 | + | ret := m.ctrl.Call(m, "MkdirAll", path, perm) | |
| 93 | + | ret0, _ := ret[0].(error) | |
| 94 | + | return ret0 | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // MkdirAll indicates an expected call of MkdirAll. | |
| 98 | + | func (mr *MockFileSystemMockRecorder) MkdirAll(path, perm any) *gomock.Call { | |
| 99 | + | mr.mock.ctrl.T.Helper() | |
| 100 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*MockFileSystem)(nil).MkdirAll), path, perm) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // ReadFile mocks base method. | |
| 104 | + | func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { | |
| 105 | + | m.ctrl.T.Helper() | |
| 106 | + | ret := m.ctrl.Call(m, "ReadFile", name) | |
| 107 | + | ret0, _ := ret[0].([]byte) | |
| 108 | + | ret1, _ := ret[1].(error) | |
| 109 | + | return ret0, ret1 | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // ReadFile indicates an expected call of ReadFile. | |
| 113 | + | func (mr *MockFileSystemMockRecorder) ReadFile(name any) *gomock.Call { | |
| 114 | + | mr.mock.ctrl.T.Helper() | |
| 115 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockFileSystem)(nil).ReadFile), name) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // RemoveAll mocks base method. | |
| 119 | + | func (m *MockFileSystem) RemoveAll(path string) error { | |
| 120 | + | m.ctrl.T.Helper() | |
| 121 | + | ret := m.ctrl.Call(m, "RemoveAll", path) | |
| 122 | + | ret0, _ := ret[0].(error) | |
| 123 | + | return ret0 | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // RemoveAll indicates an expected call of RemoveAll. | |
| 127 | + | func (mr *MockFileSystemMockRecorder) RemoveAll(path any) *gomock.Call { | |
| 128 | + | mr.mock.ctrl.T.Helper() | |
| 129 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MockFileSystem)(nil).RemoveAll), path) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Stat mocks base method. | |
| 133 | + | func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { | |
| 134 | + | m.ctrl.T.Helper() | |
| 135 | + | ret := m.ctrl.Call(m, "Stat", name) | |
| 136 | + | ret0, _ := ret[0].(os.FileInfo) | |
| 137 | + | ret1, _ := ret[1].(error) | |
| 138 | + | return ret0, ret1 | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | // Stat indicates an expected call of Stat. | |
| 142 | + | func (mr *MockFileSystemMockRecorder) Stat(name any) *gomock.Call { | |
| 143 | + | mr.mock.ctrl.T.Helper() | |
| 144 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockFileSystem)(nil).Stat), name) | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // WriteFile mocks base method. | |
| 148 | + | func (m *MockFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error { | |
| 149 | + | m.ctrl.T.Helper() | |
| 150 | + | ret := m.ctrl.Call(m, "WriteFile", name, data, perm) | |
| 151 | + | ret0, _ := ret[0].(error) | |
| 152 | + | return ret0 | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // WriteFile indicates an expected call of WriteFile. | |
| 156 | + | func (mr *MockFileSystemMockRecorder) WriteFile(name, data, perm any) *gomock.Call { | |
| 157 | + | mr.mock.ctrl.T.Helper() | |
| 158 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteFile", reflect.TypeOf((*MockFileSystem)(nil).WriteFile), name, data, perm) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // MockFile is a mock of File interface. | |
| 162 | + | type MockFile struct { | |
| 163 | + | ctrl *gomock.Controller | |
| 164 | + | recorder *MockFileMockRecorder | |
| 165 | + | isgomock struct{} | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // MockFileMockRecorder is the mock recorder for MockFile. | |
| 169 | + | type MockFileMockRecorder struct { | |
| 170 | + | mock *MockFile | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | // NewMockFile creates a new mock instance. | |
| 174 | + | func NewMockFile(ctrl *gomock.Controller) *MockFile { | |
| 175 | + | mock := &MockFile{ctrl: ctrl} | |
| 176 | + | mock.recorder = &MockFileMockRecorder{mock} | |
| 177 | + | return mock | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 181 | + | func (m *MockFile) EXPECT() *MockFileMockRecorder { | |
| 182 | + | return m.recorder | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // Close mocks base method. | |
| 186 | + | func (m *MockFile) Close() error { | |
| 187 | + | m.ctrl.T.Helper() | |
| 188 | + | ret := m.ctrl.Call(m, "Close") | |
| 189 | + | ret0, _ := ret[0].(error) | |
| 190 | + | return ret0 | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // Close indicates an expected call of Close. | |
| 194 | + | func (mr *MockFileMockRecorder) Close() *gomock.Call { | |
| 195 | + | mr.mock.ctrl.T.Helper() | |
| 196 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockFile)(nil).Close)) | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // Read mocks base method. | |
| 200 | + | func (m *MockFile) Read(b []byte) (int, error) { | |
| 201 | + | m.ctrl.T.Helper() | |
| 202 | + | ret := m.ctrl.Call(m, "Read", b) | |
| 203 | + | ret0, _ := ret[0].(int) | |
| 204 | + | ret1, _ := ret[1].(error) | |
| 205 | + | return ret0, ret1 | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // Read indicates an expected call of Read. | |
| 209 | + | func (mr *MockFileMockRecorder) Read(b any) *gomock.Call { | |
| 210 | + | mr.mock.ctrl.T.Helper() | |
| 211 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockFile)(nil).Read), b) | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // Write mocks base method. | |
| 215 | + | func (m *MockFile) Write(b []byte) (int, error) { | |
| 216 | + | m.ctrl.T.Helper() | |
| 217 | + | ret := m.ctrl.Call(m, "Write", b) | |
| 218 | + | ret0, _ := ret[0].(int) | |
| 219 | + | ret1, _ := ret[1].(error) | |
| 220 | + | return ret0, ret1 | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // Write indicates an expected call of Write. | |
| 224 | + | func (mr *MockFileMockRecorder) Write(b any) *gomock.Call { | |
| 225 | + | mr.mock.ctrl.T.Helper() | |
| 226 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockFile)(nil).Write), b) | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // MockCommandExecutor is a mock of CommandExecutor interface. | |
| 230 | + | type MockCommandExecutor struct { | |
| 231 | + | ctrl *gomock.Controller | |
| 232 | + | recorder *MockCommandExecutorMockRecorder | |
| 233 | + | isgomock struct{} | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // MockCommandExecutorMockRecorder is the mock recorder for MockCommandExecutor. | |
| 237 | + | type MockCommandExecutorMockRecorder struct { | |
| 238 | + | mock *MockCommandExecutor | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | // NewMockCommandExecutor creates a new mock instance. | |
| 242 | + | func NewMockCommandExecutor(ctrl *gomock.Controller) *MockCommandExecutor { | |
| 243 | + | mock := &MockCommandExecutor{ctrl: ctrl} | |
| 244 | + | mock.recorder = &MockCommandExecutorMockRecorder{mock} | |
| 245 | + | return mock | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 249 | + | func (m *MockCommandExecutor) EXPECT() *MockCommandExecutorMockRecorder { | |
| 250 | + | return m.recorder | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // Execute mocks base method. | |
| 254 | + | func (m *MockCommandExecutor) Execute(ctx context.Context, command string, args ...string) ([]byte, error) { | |
| 255 | + | m.ctrl.T.Helper() | |
| 256 | + | varargs := []any{ctx, command} | |
| 257 | + | for _, a := range args { | |
| 258 | + | varargs = append(varargs, a) | |
| 259 | + | } | |
| 260 | + | ret := m.ctrl.Call(m, "Execute", varargs...) | |
| 261 | + | ret0, _ := ret[0].([]byte) | |
| 262 | + | ret1, _ := ret[1].(error) | |
| 263 | + | return ret0, ret1 | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | // Execute indicates an expected call of Execute. | |
| 267 | + | func (mr *MockCommandExecutorMockRecorder) Execute(ctx, command any, args ...any) *gomock.Call { | |
| 268 | + | mr.mock.ctrl.T.Helper() | |
| 269 | + | varargs := append([]any{ctx, command}, args...) | |
| 270 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockCommandExecutor)(nil).Execute), varargs...) | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // ExecuteWithEnv mocks base method. | |
| 274 | + | func (m *MockCommandExecutor) ExecuteWithEnv(ctx context.Context, env map[string]string, command string, args ...string) ([]byte, error) { | |
| 275 | + | m.ctrl.T.Helper() | |
| 276 | + | varargs := []any{ctx, env, command} | |
| 277 | + | for _, a := range args { | |
| 278 | + | varargs = append(varargs, a) | |
| 279 | + | } | |
| 280 | + | ret := m.ctrl.Call(m, "ExecuteWithEnv", varargs...) | |
| 281 | + | ret0, _ := ret[0].([]byte) | |
| 282 | + | ret1, _ := ret[1].(error) | |
| 283 | + | return ret0, ret1 | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // ExecuteWithEnv indicates an expected call of ExecuteWithEnv. | |
| 287 | + | func (mr *MockCommandExecutorMockRecorder) ExecuteWithEnv(ctx, env, command any, args ...any) *gomock.Call { | |
| 288 | + | mr.mock.ctrl.T.Helper() | |
| 289 | + | varargs := append([]any{ctx, env, command}, args...) | |
| 290 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteWithEnv", reflect.TypeOf((*MockCommandExecutor)(nil).ExecuteWithEnv), varargs...) | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | // MockChrootExecutor is a mock of ChrootExecutor interface. | |
| 294 | + | type MockChrootExecutor struct { | |
| 295 | + | ctrl *gomock.Controller | |
| 296 | + | recorder *MockChrootExecutorMockRecorder | |
| 297 | + | isgomock struct{} | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | // MockChrootExecutorMockRecorder is the mock recorder for MockChrootExecutor. | |
| 301 | + | type MockChrootExecutorMockRecorder struct { | |
| 302 | + | mock *MockChrootExecutor | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // NewMockChrootExecutor creates a new mock instance. | |
| 306 | + | func NewMockChrootExecutor(ctrl *gomock.Controller) *MockChrootExecutor { | |
| 307 | + | mock := &MockChrootExecutor{ctrl: ctrl} | |
| 308 | + | mock.recorder = &MockChrootExecutorMockRecorder{mock} | |
| 309 | + | return mock | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 313 | + | func (m *MockChrootExecutor) EXPECT() *MockChrootExecutorMockRecorder { | |
| 314 | + | return m.recorder | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | // ChrootSystemctl mocks base method. | |
| 318 | + | func (m *MockChrootExecutor) ChrootSystemctl(logPath, chrootPath string, args ...string) error { | |
| 319 | + | m.ctrl.T.Helper() | |
| 320 | + | varargs := []any{logPath, chrootPath} | |
| 321 | + | for _, a := range args { | |
| 322 | + | varargs = append(varargs, a) | |
| 323 | + | } | |
| 324 | + | ret := m.ctrl.Call(m, "ChrootSystemctl", varargs...) | |
| 325 | + | ret0, _ := ret[0].(error) | |
| 326 | + | return ret0 | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | // ChrootSystemctl indicates an expected call of ChrootSystemctl. | |
| 330 | + | func (mr *MockChrootExecutorMockRecorder) ChrootSystemctl(logPath, chrootPath any, args ...any) *gomock.Call { | |
| 331 | + | mr.mock.ctrl.T.Helper() | |
| 332 | + | varargs := append([]any{logPath, chrootPath}, args...) | |
| 333 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChrootSystemctl", reflect.TypeOf((*MockChrootExecutor)(nil).ChrootSystemctl), varargs...) | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | // ExecuteInChroot mocks base method. | |
| 337 | + | func (m *MockChrootExecutor) ExecuteInChroot(ctx context.Context, chrootPath, command string, args ...string) ([]byte, error) { | |
| 338 | + | m.ctrl.T.Helper() | |
| 339 | + | varargs := []any{ctx, chrootPath, command} | |
| 340 | + | for _, a := range args { | |
| 341 | + | varargs = append(varargs, a) | |
| 342 | + | } | |
| 343 | + | ret := m.ctrl.Call(m, "ExecuteInChroot", varargs...) | |
| 344 | + | ret0, _ := ret[0].([]byte) | |
| 345 | + | ret1, _ := ret[1].(error) | |
| 346 | + | return ret0, ret1 | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | // ExecuteInChroot indicates an expected call of ExecuteInChroot. | |
| 350 | + | func (mr *MockChrootExecutorMockRecorder) ExecuteInChroot(ctx, chrootPath, command any, args ...any) *gomock.Call { | |
| 351 | + | mr.mock.ctrl.T.Helper() | |
| 352 | + | varargs := append([]any{ctx, chrootPath, command}, args...) | |
| 353 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteInChroot", reflect.TypeOf((*MockChrootExecutor)(nil).ExecuteInChroot), varargs...) | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // MockScriptExecutor is a mock of ScriptExecutor interface. | |
| 357 | + | type MockScriptExecutor struct { | |
| 358 | + | ctrl *gomock.Controller | |
| 359 | + | recorder *MockScriptExecutorMockRecorder | |
| 360 | + | isgomock struct{} | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | // MockScriptExecutorMockRecorder is the mock recorder for MockScriptExecutor. | |
| 364 | + | type MockScriptExecutorMockRecorder struct { | |
| 365 | + | mock *MockScriptExecutor | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | // NewMockScriptExecutor creates a new mock instance. | |
| 369 | + | func NewMockScriptExecutor(ctrl *gomock.Controller) *MockScriptExecutor { | |
| 370 | + | mock := &MockScriptExecutor{ctrl: ctrl} | |
| 371 | + | mock.recorder = &MockScriptExecutorMockRecorder{mock} | |
| 372 | + | return mock | |
| 373 | + | } | |
| 374 | + | ||
| 375 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 376 | + | func (m *MockScriptExecutor) EXPECT() *MockScriptExecutorMockRecorder { | |
| 377 | + | return m.recorder | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | // ExecuteScript mocks base method. | |
| 381 | + | func (m *MockScriptExecutor) ExecuteScript(ctx context.Context, scriptPath string, env map[string]string) error { | |
| 382 | + | m.ctrl.T.Helper() | |
| 383 | + | ret := m.ctrl.Call(m, "ExecuteScript", ctx, scriptPath, env) | |
| 384 | + | ret0, _ := ret[0].(error) | |
| 385 | + | return ret0 | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | // ExecuteScript indicates an expected call of ExecuteScript. | |
| 389 | + | func (mr *MockScriptExecutorMockRecorder) ExecuteScript(ctx, scriptPath, env any) *gomock.Call { | |
| 390 | + | mr.mock.ctrl.T.Helper() | |
| 391 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteScript", reflect.TypeOf((*MockScriptExecutor)(nil).ExecuteScript), ctx, scriptPath, env) | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | // MockHTTPClient is a mock of HTTPClient interface. | |
| 395 | + | type MockHTTPClient struct { | |
| 396 | + | ctrl *gomock.Controller | |
| 397 | + | recorder *MockHTTPClientMockRecorder | |
| 398 | + | isgomock struct{} | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | // MockHTTPClientMockRecorder is the mock recorder for MockHTTPClient. | |
| 402 | + | type MockHTTPClientMockRecorder struct { | |
| 403 | + | mock *MockHTTPClient | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | // NewMockHTTPClient creates a new mock instance. | |
| 407 | + | func NewMockHTTPClient(ctrl *gomock.Controller) *MockHTTPClient { | |
| 408 | + | mock := &MockHTTPClient{ctrl: ctrl} | |
| 409 | + | mock.recorder = &MockHTTPClientMockRecorder{mock} | |
| 410 | + | return mock | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 414 | + | func (m *MockHTTPClient) EXPECT() *MockHTTPClientMockRecorder { | |
| 415 | + | return m.recorder | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | // Get mocks base method. | |
| 419 | + | func (m *MockHTTPClient) Get(url string) (ports.Response, error) { | |
| 420 | + | m.ctrl.T.Helper() | |
| 421 | + | ret := m.ctrl.Call(m, "Get", url) | |
| 422 | + | ret0, _ := ret[0].(ports.Response) | |
| 423 | + | ret1, _ := ret[1].(error) | |
| 424 | + | return ret0, ret1 | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | // Get indicates an expected call of Get. | |
| 428 | + | func (mr *MockHTTPClientMockRecorder) Get(url any) *gomock.Call { | |
| 429 | + | mr.mock.ctrl.T.Helper() | |
| 430 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockHTTPClient)(nil).Get), url) | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | // Post mocks base method. | |
| 434 | + | func (m *MockHTTPClient) Post(url, contentType string, body []byte) (ports.Response, error) { | |
| 435 | + | m.ctrl.T.Helper() | |
| 436 | + | ret := m.ctrl.Call(m, "Post", url, contentType, body) | |
| 437 | + | ret0, _ := ret[0].(ports.Response) | |
| 438 | + | ret1, _ := ret[1].(error) | |
| 439 | + | return ret0, ret1 | |
| 440 | + | } | |
| 441 | + | ||
| 442 | + | // Post indicates an expected call of Post. | |
| 443 | + | func (mr *MockHTTPClientMockRecorder) Post(url, contentType, body any) *gomock.Call { | |
| 444 | + | mr.mock.ctrl.T.Helper() | |
| 445 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockHTTPClient)(nil).Post), url, contentType, body) | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | // MockResponse is a mock of Response interface. | |
| 449 | + | type MockResponse struct { | |
| 450 | + | ctrl *gomock.Controller | |
| 451 | + | recorder *MockResponseMockRecorder | |
| 452 | + | isgomock struct{} | |
| 453 | + | } | |
| 454 | + | ||
| 455 | + | // MockResponseMockRecorder is the mock recorder for MockResponse. | |
| 456 | + | type MockResponseMockRecorder struct { | |
| 457 | + | mock *MockResponse | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | // NewMockResponse creates a new mock instance. | |
| 461 | + | func NewMockResponse(ctrl *gomock.Controller) *MockResponse { | |
| 462 | + | mock := &MockResponse{ctrl: ctrl} | |
| 463 | + | mock.recorder = &MockResponseMockRecorder{mock} | |
| 464 | + | return mock | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 468 | + | func (m *MockResponse) EXPECT() *MockResponseMockRecorder { | |
| 469 | + | return m.recorder | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | // Body mocks base method. | |
| 473 | + | func (m *MockResponse) Body() []byte { | |
| 474 | + | m.ctrl.T.Helper() | |
| 475 | + | ret := m.ctrl.Call(m, "Body") | |
| 476 | + | ret0, _ := ret[0].([]byte) | |
| 477 | + | return ret0 | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | // Body indicates an expected call of Body. | |
| 481 | + | func (mr *MockResponseMockRecorder) Body() *gomock.Call { | |
| 482 | + | mr.mock.ctrl.T.Helper() | |
| 483 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Body", reflect.TypeOf((*MockResponse)(nil).Body)) | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | // Close mocks base method. | |
| 487 | + | func (m *MockResponse) Close() error { | |
| 488 | + | m.ctrl.T.Helper() | |
| 489 | + | ret := m.ctrl.Call(m, "Close") | |
| 490 | + | ret0, _ := ret[0].(error) | |
| 491 | + | return ret0 | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | // Close indicates an expected call of Close. | |
| 495 | + | func (mr *MockResponseMockRecorder) Close() *gomock.Call { | |
| 496 | + | mr.mock.ctrl.T.Helper() | |
| 497 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockResponse)(nil).Close)) | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | // StatusCode mocks base method. | |
| 501 | + | func (m *MockResponse) StatusCode() int { | |
| 502 | + | m.ctrl.T.Helper() | |
| 503 | + | ret := m.ctrl.Call(m, "StatusCode") | |
| 504 | + | ret0, _ := ret[0].(int) | |
| 505 | + | return ret0 | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // StatusCode indicates an expected call of StatusCode. | |
| 509 | + | func (mr *MockResponseMockRecorder) StatusCode() *gomock.Call { | |
| 510 | + | mr.mock.ctrl.T.Helper() | |
| 511 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatusCode", reflect.TypeOf((*MockResponse)(nil).StatusCode)) | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | // MockLogger is a mock of Logger interface. | |
| 515 | + | type MockLogger struct { | |
| 516 | + | ctrl *gomock.Controller | |
| 517 | + | recorder *MockLoggerMockRecorder | |
| 518 | + | isgomock struct{} | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | // MockLoggerMockRecorder is the mock recorder for MockLogger. | |
| 522 | + | type MockLoggerMockRecorder struct { | |
| 523 | + | mock *MockLogger | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | // NewMockLogger creates a new mock instance. | |
| 527 | + | func NewMockLogger(ctrl *gomock.Controller) *MockLogger { | |
| 528 | + | mock := &MockLogger{ctrl: ctrl} | |
| 529 | + | mock.recorder = &MockLoggerMockRecorder{mock} | |
| 530 | + | return mock | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 534 | + | func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { | |
| 535 | + | return m.recorder | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | // Debug mocks base method. | |
| 539 | + | func (m *MockLogger) Debug(msg string, keysAndValues ...any) { | |
| 540 | + | m.ctrl.T.Helper() | |
| 541 | + | varargs := []any{msg} | |
| 542 | + | for _, a := range keysAndValues { | |
| 543 | + | varargs = append(varargs, a) | |
| 544 | + | } | |
| 545 | + | m.ctrl.Call(m, "Debug", varargs...) | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | // Debug indicates an expected call of Debug. | |
| 549 | + | func (mr *MockLoggerMockRecorder) Debug(msg any, keysAndValues ...any) *gomock.Call { | |
| 550 | + | mr.mock.ctrl.T.Helper() | |
| 551 | + | varargs := append([]any{msg}, keysAndValues...) | |
| 552 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) | |
| 553 | + | } | |
| 554 | + | ||
| 555 | + | // Error mocks base method. | |
| 556 | + | func (m *MockLogger) Error(msg string, keysAndValues ...any) { | |
| 557 | + | m.ctrl.T.Helper() | |
| 558 | + | varargs := []any{msg} | |
| 559 | + | for _, a := range keysAndValues { | |
| 560 | + | varargs = append(varargs, a) | |
| 561 | + | } | |
| 562 | + | m.ctrl.Call(m, "Error", varargs...) | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | // Error indicates an expected call of Error. | |
| 566 | + | func (mr *MockLoggerMockRecorder) Error(msg any, keysAndValues ...any) *gomock.Call { | |
| 567 | + | mr.mock.ctrl.T.Helper() | |
| 568 | + | varargs := append([]any{msg}, keysAndValues...) | |
| 569 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) | |
| 570 | + | } | |
| 571 | + | ||
| 572 | + | // Info mocks base method. | |
| 573 | + | func (m *MockLogger) Info(msg string, keysAndValues ...any) { | |
| 574 | + | m.ctrl.T.Helper() | |
| 575 | + | varargs := []any{msg} | |
| 576 | + | for _, a := range keysAndValues { | |
| 577 | + | varargs = append(varargs, a) | |
| 578 | + | } | |
| 579 | + | m.ctrl.Call(m, "Info", varargs...) | |
| 580 | + | } | |
| 581 | + | ||
| 582 | + | // Info indicates an expected call of Info. | |
| 583 | + | func (mr *MockLoggerMockRecorder) Info(msg any, keysAndValues ...any) *gomock.Call { | |
| 584 | + | mr.mock.ctrl.T.Helper() | |
| 585 | + | varargs := append([]any{msg}, keysAndValues...) | |
| 586 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) | |
| 587 | + | } | |
| 588 | + | ||
| 589 | + | // LogPath mocks base method. | |
| 590 | + | func (m *MockLogger) LogPath() string { | |
| 591 | + | m.ctrl.T.Helper() | |
| 592 | + | ret := m.ctrl.Call(m, "LogPath") | |
| 593 | + | ret0, _ := ret[0].(string) | |
| 594 | + | return ret0 | |
| 595 | + | } | |
| 596 | + | ||
| 597 | + | // LogPath indicates an expected call of LogPath. | |
| 598 | + | func (mr *MockLoggerMockRecorder) LogPath() *gomock.Call { | |
| 599 | + | mr.mock.ctrl.T.Helper() | |
| 600 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogPath", reflect.TypeOf((*MockLogger)(nil).LogPath)) | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | // Warn mocks base method. | |
| 604 | + | func (m *MockLogger) Warn(msg string, keysAndValues ...any) { | |
| 605 | + | m.ctrl.T.Helper() | |
| 606 | + | varargs := []any{msg} | |
| 607 | + | for _, a := range keysAndValues { | |
| 608 | + | varargs = append(varargs, a) | |
| 609 | + | } | |
| 610 | + | m.ctrl.Call(m, "Warn", varargs...) | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | // Warn indicates an expected call of Warn. | |
| 614 | + | func (mr *MockLoggerMockRecorder) Warn(msg any, keysAndValues ...any) *gomock.Call { | |
| 615 | + | mr.mock.ctrl.T.Helper() | |
| 616 | + | varargs := append([]any{msg}, keysAndValues...) | |
| 617 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) | |
| 618 | + | } | |
| 619 | + | ||
| 620 | + | // MockInstallationRepository is a mock of InstallationRepository interface. | |
| 621 | + | type MockInstallationRepository struct { | |
| 622 | + | ctrl *gomock.Controller | |
| 623 | + | recorder *MockInstallationRepositoryMockRecorder | |
| 624 | + | isgomock struct{} | |
| 625 | + | } | |
| 626 | + | ||
| 627 | + | // MockInstallationRepositoryMockRecorder is the mock recorder for MockInstallationRepository. | |
| 628 | + | type MockInstallationRepositoryMockRecorder struct { | |
| 629 | + | mock *MockInstallationRepository | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | // NewMockInstallationRepository creates a new mock instance. | |
| 633 | + | func NewMockInstallationRepository(ctrl *gomock.Controller) *MockInstallationRepository { | |
| 634 | + | mock := &MockInstallationRepository{ctrl: ctrl} | |
| 635 | + | mock.recorder = &MockInstallationRepositoryMockRecorder{mock} | |
| 636 | + | return mock | |
| 637 | + | } | |
| 638 | + | ||
| 639 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |
| 640 | + | func (m *MockInstallationRepository) EXPECT() *MockInstallationRepositoryMockRecorder { | |
| 641 | + | return m.recorder | |
| 642 | + | } | |
| 643 | + | ||
| 644 | + | // Exists mocks base method. | |
| 645 | + | func (m *MockInstallationRepository) Exists(ctx context.Context, installationID string) (bool, error) { | |
| 646 | + | m.ctrl.T.Helper() | |
| 647 | + | ret := m.ctrl.Call(m, "Exists", ctx, installationID) | |
| 648 | + | ret0, _ := ret[0].(bool) | |
| 649 | + | ret1, _ := ret[1].(error) | |
| 650 | + | return ret0, ret1 | |
| 651 | + | } | |
| 652 | + | ||
| 653 | + | // Exists indicates an expected call of Exists. | |
| 654 | + | func (mr *MockInstallationRepositoryMockRecorder) Exists(ctx, installationID any) *gomock.Call { | |
| 655 | + | mr.mock.ctrl.T.Helper() | |
| 656 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockInstallationRepository)(nil).Exists), ctx, installationID) | |
| 657 | + | } | |
| 658 | + | ||
| 659 | + | // Load mocks base method. | |
| 660 | + | func (m *MockInstallationRepository) Load(ctx context.Context, installationID string) (string, error) { | |
| 661 | + | m.ctrl.T.Helper() | |
| 662 | + | ret := m.ctrl.Call(m, "Load", ctx, installationID) | |
| 663 | + | ret0, _ := ret[0].(string) | |
| 664 | + | ret1, _ := ret[1].(error) | |
| 665 | + | return ret0, ret1 | |
| 666 | + | } | |
| 667 | + | ||
| 668 | + | // Load indicates an expected call of Load. | |
| 669 | + | func (mr *MockInstallationRepositoryMockRecorder) Load(ctx, installationID any) *gomock.Call { | |
| 670 | + | mr.mock.ctrl.T.Helper() | |
| 671 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockInstallationRepository)(nil).Load), ctx, installationID) | |
| 672 | + | } | |
| 673 | + | ||
| 674 | + | // Save mocks base method. | |
| 675 | + | func (m *MockInstallationRepository) Save(ctx context.Context, installationID, state string) error { | |
| 676 | + | m.ctrl.T.Helper() | |
| 677 | + | ret := m.ctrl.Call(m, "Save", ctx, installationID, state) | |
| 678 | + | ret0, _ := ret[0].(error) | |
| 679 | + | return ret0 | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | // Save indicates an expected call of Save. | |
| 683 | + | func (mr *MockInstallationRepositoryMockRecorder) Save(ctx, installationID, state any) *gomock.Call { | |
| 684 | + | mr.mock.ctrl.T.Helper() | |
| 685 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInstallationRepository)(nil).Save), ctx, installationID, state) | |
| 686 | + | } | |
| @@ -0,0 +1,686 @@ | |||||
| 1 | + | // Code generated by MockGen. DO NOT EDIT. | |||
| 2 | + | // Source: github.com/bnema/archup/internal/domain/ports (interfaces: FileSystem,File,CommandExecutor,ChrootExecutor,ScriptExecutor,HTTPClient,Response,Logger,InstallationRepository) | |||
| 3 | + | // | |||
| 4 | + | // Generated by this command: | |||
| 5 | + | // | |||
| 6 | + | // mockgen -destination=mocks/mock_ports.go -package=mocks . FileSystem,File,CommandExecutor,ChrootExecutor,ScriptExecutor,HTTPClient,Response,Logger,InstallationRepository | |||
| 7 | + | // | |||
| 8 | + | ||||
| 9 | + | // Package mocks is a generated GoMock package. | |||
| 10 | + | package mocks | |||
| 11 | + | ||||
| 12 | + | import ( | |||
| 13 | + | context "context" | |||
| 14 | + | os "os" | |||
| 15 | + | reflect "reflect" | |||
| 16 | + | ||||
| 17 | + | ports "github.com/bnema/archup/internal/domain/ports" | |||
| 18 | + | gomock "go.uber.org/mock/gomock" | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // MockFileSystem is a mock of FileSystem interface. | |||
| 22 | + | type MockFileSystem struct { | |||
| 23 | + | ctrl *gomock.Controller | |||
| 24 | + | recorder *MockFileSystemMockRecorder | |||
| 25 | + | isgomock struct{} | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // MockFileSystemMockRecorder is the mock recorder for MockFileSystem. | |||
| 29 | + | type MockFileSystemMockRecorder struct { | |||
| 30 | + | mock *MockFileSystem | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // NewMockFileSystem creates a new mock instance. | |||
| 34 | + | func NewMockFileSystem(ctrl *gomock.Controller) *MockFileSystem { | |||
| 35 | + | mock := &MockFileSystem{ctrl: ctrl} | |||
| 36 | + | mock.recorder = &MockFileSystemMockRecorder{mock} | |||
| 37 | + | return mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 41 | + | func (m *MockFileSystem) EXPECT() *MockFileSystemMockRecorder { | |||
| 42 | + | return m.recorder | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // Chmod mocks base method. | |||
| 46 | + | func (m *MockFileSystem) Chmod(name string, perm os.FileMode) error { | |||
| 47 | + | m.ctrl.T.Helper() | |||
| 48 | + | ret := m.ctrl.Call(m, "Chmod", name, perm) | |||
| 49 | + | ret0, _ := ret[0].(error) | |||
| 50 | + | return ret0 | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // Chmod indicates an expected call of Chmod. | |||
| 54 | + | func (mr *MockFileSystemMockRecorder) Chmod(name, perm any) *gomock.Call { | |||
| 55 | + | mr.mock.ctrl.T.Helper() | |||
| 56 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chmod", reflect.TypeOf((*MockFileSystem)(nil).Chmod), name, perm) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // Create mocks base method. | |||
| 60 | + | func (m *MockFileSystem) Create(name string) (ports.File, error) { | |||
| 61 | + | m.ctrl.T.Helper() | |||
| 62 | + | ret := m.ctrl.Call(m, "Create", name) | |||
| 63 | + | ret0, _ := ret[0].(ports.File) | |||
| 64 | + | ret1, _ := ret[1].(error) | |||
| 65 | + | return ret0, ret1 | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // Create indicates an expected call of Create. | |||
| 69 | + | func (mr *MockFileSystemMockRecorder) Create(name any) *gomock.Call { | |||
| 70 | + | mr.mock.ctrl.T.Helper() | |||
| 71 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFileSystem)(nil).Create), name) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // Exists mocks base method. | |||
| 75 | + | func (m *MockFileSystem) Exists(path string) (bool, error) { | |||
| 76 | + | m.ctrl.T.Helper() | |||
| 77 | + | ret := m.ctrl.Call(m, "Exists", path) | |||
| 78 | + | ret0, _ := ret[0].(bool) | |||
| 79 | + | ret1, _ := ret[1].(error) | |||
| 80 | + | return ret0, ret1 | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // Exists indicates an expected call of Exists. | |||
| 84 | + | func (mr *MockFileSystemMockRecorder) Exists(path any) *gomock.Call { | |||
| 85 | + | mr.mock.ctrl.T.Helper() | |||
| 86 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockFileSystem)(nil).Exists), path) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // MkdirAll mocks base method. | |||
| 90 | + | func (m *MockFileSystem) MkdirAll(path string, perm os.FileMode) error { | |||
| 91 | + | m.ctrl.T.Helper() | |||
| 92 | + | ret := m.ctrl.Call(m, "MkdirAll", path, perm) | |||
| 93 | + | ret0, _ := ret[0].(error) | |||
| 94 | + | return ret0 | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // MkdirAll indicates an expected call of MkdirAll. | |||
| 98 | + | func (mr *MockFileSystemMockRecorder) MkdirAll(path, perm any) *gomock.Call { | |||
| 99 | + | mr.mock.ctrl.T.Helper() | |||
| 100 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*MockFileSystem)(nil).MkdirAll), path, perm) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | // ReadFile mocks base method. | |||
| 104 | + | func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { | |||
| 105 | + | m.ctrl.T.Helper() | |||
| 106 | + | ret := m.ctrl.Call(m, "ReadFile", name) | |||
| 107 | + | ret0, _ := ret[0].([]byte) | |||
| 108 | + | ret1, _ := ret[1].(error) | |||
| 109 | + | return ret0, ret1 | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // ReadFile indicates an expected call of ReadFile. | |||
| 113 | + | func (mr *MockFileSystemMockRecorder) ReadFile(name any) *gomock.Call { | |||
| 114 | + | mr.mock.ctrl.T.Helper() | |||
| 115 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockFileSystem)(nil).ReadFile), name) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // RemoveAll mocks base method. | |||
| 119 | + | func (m *MockFileSystem) RemoveAll(path string) error { | |||
| 120 | + | m.ctrl.T.Helper() | |||
| 121 | + | ret := m.ctrl.Call(m, "RemoveAll", path) | |||
| 122 | + | ret0, _ := ret[0].(error) | |||
| 123 | + | return ret0 | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // RemoveAll indicates an expected call of RemoveAll. | |||
| 127 | + | func (mr *MockFileSystemMockRecorder) RemoveAll(path any) *gomock.Call { | |||
| 128 | + | mr.mock.ctrl.T.Helper() | |||
| 129 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MockFileSystem)(nil).RemoveAll), path) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Stat mocks base method. | |||
| 133 | + | func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { | |||
| 134 | + | m.ctrl.T.Helper() | |||
| 135 | + | ret := m.ctrl.Call(m, "Stat", name) | |||
| 136 | + | ret0, _ := ret[0].(os.FileInfo) | |||
| 137 | + | ret1, _ := ret[1].(error) | |||
| 138 | + | return ret0, ret1 | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // Stat indicates an expected call of Stat. | |||
| 142 | + | func (mr *MockFileSystemMockRecorder) Stat(name any) *gomock.Call { | |||
| 143 | + | mr.mock.ctrl.T.Helper() | |||
| 144 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockFileSystem)(nil).Stat), name) | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // WriteFile mocks base method. | |||
| 148 | + | func (m *MockFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error { | |||
| 149 | + | m.ctrl.T.Helper() | |||
| 150 | + | ret := m.ctrl.Call(m, "WriteFile", name, data, perm) | |||
| 151 | + | ret0, _ := ret[0].(error) | |||
| 152 | + | return ret0 | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // WriteFile indicates an expected call of WriteFile. | |||
| 156 | + | func (mr *MockFileSystemMockRecorder) WriteFile(name, data, perm any) *gomock.Call { | |||
| 157 | + | mr.mock.ctrl.T.Helper() | |||
| 158 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteFile", reflect.TypeOf((*MockFileSystem)(nil).WriteFile), name, data, perm) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // MockFile is a mock of File interface. | |||
| 162 | + | type MockFile struct { | |||
| 163 | + | ctrl *gomock.Controller | |||
| 164 | + | recorder *MockFileMockRecorder | |||
| 165 | + | isgomock struct{} | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // MockFileMockRecorder is the mock recorder for MockFile. | |||
| 169 | + | type MockFileMockRecorder struct { | |||
| 170 | + | mock *MockFile | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | // NewMockFile creates a new mock instance. | |||
| 174 | + | func NewMockFile(ctrl *gomock.Controller) *MockFile { | |||
| 175 | + | mock := &MockFile{ctrl: ctrl} | |||
| 176 | + | mock.recorder = &MockFileMockRecorder{mock} | |||
| 177 | + | return mock | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 181 | + | func (m *MockFile) EXPECT() *MockFileMockRecorder { | |||
| 182 | + | return m.recorder | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // Close mocks base method. | |||
| 186 | + | func (m *MockFile) Close() error { | |||
| 187 | + | m.ctrl.T.Helper() | |||
| 188 | + | ret := m.ctrl.Call(m, "Close") | |||
| 189 | + | ret0, _ := ret[0].(error) | |||
| 190 | + | return ret0 | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | // Close indicates an expected call of Close. | |||
| 194 | + | func (mr *MockFileMockRecorder) Close() *gomock.Call { | |||
| 195 | + | mr.mock.ctrl.T.Helper() | |||
| 196 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockFile)(nil).Close)) | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // Read mocks base method. | |||
| 200 | + | func (m *MockFile) Read(b []byte) (int, error) { | |||
| 201 | + | m.ctrl.T.Helper() | |||
| 202 | + | ret := m.ctrl.Call(m, "Read", b) | |||
| 203 | + | ret0, _ := ret[0].(int) | |||
| 204 | + | ret1, _ := ret[1].(error) | |||
| 205 | + | return ret0, ret1 | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // Read indicates an expected call of Read. | |||
| 209 | + | func (mr *MockFileMockRecorder) Read(b any) *gomock.Call { | |||
| 210 | + | mr.mock.ctrl.T.Helper() | |||
| 211 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockFile)(nil).Read), b) | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // Write mocks base method. | |||
| 215 | + | func (m *MockFile) Write(b []byte) (int, error) { | |||
| 216 | + | m.ctrl.T.Helper() | |||
| 217 | + | ret := m.ctrl.Call(m, "Write", b) | |||
| 218 | + | ret0, _ := ret[0].(int) | |||
| 219 | + | ret1, _ := ret[1].(error) | |||
| 220 | + | return ret0, ret1 | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | // Write indicates an expected call of Write. | |||
| 224 | + | func (mr *MockFileMockRecorder) Write(b any) *gomock.Call { | |||
| 225 | + | mr.mock.ctrl.T.Helper() | |||
| 226 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockFile)(nil).Write), b) | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | // MockCommandExecutor is a mock of CommandExecutor interface. | |||
| 230 | + | type MockCommandExecutor struct { | |||
| 231 | + | ctrl *gomock.Controller | |||
| 232 | + | recorder *MockCommandExecutorMockRecorder | |||
| 233 | + | isgomock struct{} | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // MockCommandExecutorMockRecorder is the mock recorder for MockCommandExecutor. | |||
| 237 | + | type MockCommandExecutorMockRecorder struct { | |||
| 238 | + | mock *MockCommandExecutor | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | // NewMockCommandExecutor creates a new mock instance. | |||
| 242 | + | func NewMockCommandExecutor(ctrl *gomock.Controller) *MockCommandExecutor { | |||
| 243 | + | mock := &MockCommandExecutor{ctrl: ctrl} | |||
| 244 | + | mock.recorder = &MockCommandExecutorMockRecorder{mock} | |||
| 245 | + | return mock | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 249 | + | func (m *MockCommandExecutor) EXPECT() *MockCommandExecutorMockRecorder { | |||
| 250 | + | return m.recorder | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // Execute mocks base method. | |||
| 254 | + | func (m *MockCommandExecutor) Execute(ctx context.Context, command string, args ...string) ([]byte, error) { | |||
| 255 | + | m.ctrl.T.Helper() | |||
| 256 | + | varargs := []any{ctx, command} | |||
| 257 | + | for _, a := range args { | |||
| 258 | + | varargs = append(varargs, a) | |||
| 259 | + | } | |||
| 260 | + | ret := m.ctrl.Call(m, "Execute", varargs...) | |||
| 261 | + | ret0, _ := ret[0].([]byte) | |||
| 262 | + | ret1, _ := ret[1].(error) | |||
| 263 | + | return ret0, ret1 | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | // Execute indicates an expected call of Execute. | |||
| 267 | + | func (mr *MockCommandExecutorMockRecorder) Execute(ctx, command any, args ...any) *gomock.Call { | |||
| 268 | + | mr.mock.ctrl.T.Helper() | |||
| 269 | + | varargs := append([]any{ctx, command}, args...) | |||
| 270 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockCommandExecutor)(nil).Execute), varargs...) | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | // ExecuteWithEnv mocks base method. | |||
| 274 | + | func (m *MockCommandExecutor) ExecuteWithEnv(ctx context.Context, env map[string]string, command string, args ...string) ([]byte, error) { | |||
| 275 | + | m.ctrl.T.Helper() | |||
| 276 | + | varargs := []any{ctx, env, command} | |||
| 277 | + | for _, a := range args { | |||
| 278 | + | varargs = append(varargs, a) | |||
| 279 | + | } | |||
| 280 | + | ret := m.ctrl.Call(m, "ExecuteWithEnv", varargs...) | |||
| 281 | + | ret0, _ := ret[0].([]byte) | |||
| 282 | + | ret1, _ := ret[1].(error) | |||
| 283 | + | return ret0, ret1 | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | // ExecuteWithEnv indicates an expected call of ExecuteWithEnv. | |||
| 287 | + | func (mr *MockCommandExecutorMockRecorder) ExecuteWithEnv(ctx, env, command any, args ...any) *gomock.Call { | |||
| 288 | + | mr.mock.ctrl.T.Helper() | |||
| 289 | + | varargs := append([]any{ctx, env, command}, args...) | |||
| 290 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteWithEnv", reflect.TypeOf((*MockCommandExecutor)(nil).ExecuteWithEnv), varargs...) | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | // MockChrootExecutor is a mock of ChrootExecutor interface. | |||
| 294 | + | type MockChrootExecutor struct { | |||
| 295 | + | ctrl *gomock.Controller | |||
| 296 | + | recorder *MockChrootExecutorMockRecorder | |||
| 297 | + | isgomock struct{} | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | // MockChrootExecutorMockRecorder is the mock recorder for MockChrootExecutor. | |||
| 301 | + | type MockChrootExecutorMockRecorder struct { | |||
| 302 | + | mock *MockChrootExecutor | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | // NewMockChrootExecutor creates a new mock instance. | |||
| 306 | + | func NewMockChrootExecutor(ctrl *gomock.Controller) *MockChrootExecutor { | |||
| 307 | + | mock := &MockChrootExecutor{ctrl: ctrl} | |||
| 308 | + | mock.recorder = &MockChrootExecutorMockRecorder{mock} | |||
| 309 | + | return mock | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 313 | + | func (m *MockChrootExecutor) EXPECT() *MockChrootExecutorMockRecorder { | |||
| 314 | + | return m.recorder | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | // ChrootSystemctl mocks base method. | |||
| 318 | + | func (m *MockChrootExecutor) ChrootSystemctl(logPath, chrootPath string, args ...string) error { | |||
| 319 | + | m.ctrl.T.Helper() | |||
| 320 | + | varargs := []any{logPath, chrootPath} | |||
| 321 | + | for _, a := range args { | |||
| 322 | + | varargs = append(varargs, a) | |||
| 323 | + | } | |||
| 324 | + | ret := m.ctrl.Call(m, "ChrootSystemctl", varargs...) | |||
| 325 | + | ret0, _ := ret[0].(error) | |||
| 326 | + | return ret0 | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | // ChrootSystemctl indicates an expected call of ChrootSystemctl. | |||
| 330 | + | func (mr *MockChrootExecutorMockRecorder) ChrootSystemctl(logPath, chrootPath any, args ...any) *gomock.Call { | |||
| 331 | + | mr.mock.ctrl.T.Helper() | |||
| 332 | + | varargs := append([]any{logPath, chrootPath}, args...) | |||
| 333 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChrootSystemctl", reflect.TypeOf((*MockChrootExecutor)(nil).ChrootSystemctl), varargs...) | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | // ExecuteInChroot mocks base method. | |||
| 337 | + | func (m *MockChrootExecutor) ExecuteInChroot(ctx context.Context, chrootPath, command string, args ...string) ([]byte, error) { | |||
| 338 | + | m.ctrl.T.Helper() | |||
| 339 | + | varargs := []any{ctx, chrootPath, command} | |||
| 340 | + | for _, a := range args { | |||
| 341 | + | varargs = append(varargs, a) | |||
| 342 | + | } | |||
| 343 | + | ret := m.ctrl.Call(m, "ExecuteInChroot", varargs...) | |||
| 344 | + | ret0, _ := ret[0].([]byte) | |||
| 345 | + | ret1, _ := ret[1].(error) | |||
| 346 | + | return ret0, ret1 | |||
| 347 | + | } | |||
| 348 | + | ||||
| 349 | + | // ExecuteInChroot indicates an expected call of ExecuteInChroot. | |||
| 350 | + | func (mr *MockChrootExecutorMockRecorder) ExecuteInChroot(ctx, chrootPath, command any, args ...any) *gomock.Call { | |||
| 351 | + | mr.mock.ctrl.T.Helper() | |||
| 352 | + | varargs := append([]any{ctx, chrootPath, command}, args...) | |||
| 353 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteInChroot", reflect.TypeOf((*MockChrootExecutor)(nil).ExecuteInChroot), varargs...) | |||
| 354 | + | } | |||
| 355 | + | ||||
| 356 | + | // MockScriptExecutor is a mock of ScriptExecutor interface. | |||
| 357 | + | type MockScriptExecutor struct { | |||
| 358 | + | ctrl *gomock.Controller | |||
| 359 | + | recorder *MockScriptExecutorMockRecorder | |||
| 360 | + | isgomock struct{} | |||
| 361 | + | } | |||
| 362 | + | ||||
| 363 | + | // MockScriptExecutorMockRecorder is the mock recorder for MockScriptExecutor. | |||
| 364 | + | type MockScriptExecutorMockRecorder struct { | |||
| 365 | + | mock *MockScriptExecutor | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | // NewMockScriptExecutor creates a new mock instance. | |||
| 369 | + | func NewMockScriptExecutor(ctrl *gomock.Controller) *MockScriptExecutor { | |||
| 370 | + | mock := &MockScriptExecutor{ctrl: ctrl} | |||
| 371 | + | mock.recorder = &MockScriptExecutorMockRecorder{mock} | |||
| 372 | + | return mock | |||
| 373 | + | } | |||
| 374 | + | ||||
| 375 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 376 | + | func (m *MockScriptExecutor) EXPECT() *MockScriptExecutorMockRecorder { | |||
| 377 | + | return m.recorder | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | // ExecuteScript mocks base method. | |||
| 381 | + | func (m *MockScriptExecutor) ExecuteScript(ctx context.Context, scriptPath string, env map[string]string) error { | |||
| 382 | + | m.ctrl.T.Helper() | |||
| 383 | + | ret := m.ctrl.Call(m, "ExecuteScript", ctx, scriptPath, env) | |||
| 384 | + | ret0, _ := ret[0].(error) | |||
| 385 | + | return ret0 | |||
| 386 | + | } | |||
| 387 | + | ||||
| 388 | + | // ExecuteScript indicates an expected call of ExecuteScript. | |||
| 389 | + | func (mr *MockScriptExecutorMockRecorder) ExecuteScript(ctx, scriptPath, env any) *gomock.Call { | |||
| 390 | + | mr.mock.ctrl.T.Helper() | |||
| 391 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteScript", reflect.TypeOf((*MockScriptExecutor)(nil).ExecuteScript), ctx, scriptPath, env) | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | // MockHTTPClient is a mock of HTTPClient interface. | |||
| 395 | + | type MockHTTPClient struct { | |||
| 396 | + | ctrl *gomock.Controller | |||
| 397 | + | recorder *MockHTTPClientMockRecorder | |||
| 398 | + | isgomock struct{} | |||
| 399 | + | } | |||
| 400 | + | ||||
| 401 | + | // MockHTTPClientMockRecorder is the mock recorder for MockHTTPClient. | |||
| 402 | + | type MockHTTPClientMockRecorder struct { | |||
| 403 | + | mock *MockHTTPClient | |||
| 404 | + | } | |||
| 405 | + | ||||
| 406 | + | // NewMockHTTPClient creates a new mock instance. | |||
| 407 | + | func NewMockHTTPClient(ctrl *gomock.Controller) *MockHTTPClient { | |||
| 408 | + | mock := &MockHTTPClient{ctrl: ctrl} | |||
| 409 | + | mock.recorder = &MockHTTPClientMockRecorder{mock} | |||
| 410 | + | return mock | |||
| 411 | + | } | |||
| 412 | + | ||||
| 413 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 414 | + | func (m *MockHTTPClient) EXPECT() *MockHTTPClientMockRecorder { | |||
| 415 | + | return m.recorder | |||
| 416 | + | } | |||
| 417 | + | ||||
| 418 | + | // Get mocks base method. | |||
| 419 | + | func (m *MockHTTPClient) Get(url string) (ports.Response, error) { | |||
| 420 | + | m.ctrl.T.Helper() | |||
| 421 | + | ret := m.ctrl.Call(m, "Get", url) | |||
| 422 | + | ret0, _ := ret[0].(ports.Response) | |||
| 423 | + | ret1, _ := ret[1].(error) | |||
| 424 | + | return ret0, ret1 | |||
| 425 | + | } | |||
| 426 | + | ||||
| 427 | + | // Get indicates an expected call of Get. | |||
| 428 | + | func (mr *MockHTTPClientMockRecorder) Get(url any) *gomock.Call { | |||
| 429 | + | mr.mock.ctrl.T.Helper() | |||
| 430 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockHTTPClient)(nil).Get), url) | |||
| 431 | + | } | |||
| 432 | + | ||||
| 433 | + | // Post mocks base method. | |||
| 434 | + | func (m *MockHTTPClient) Post(url, contentType string, body []byte) (ports.Response, error) { | |||
| 435 | + | m.ctrl.T.Helper() | |||
| 436 | + | ret := m.ctrl.Call(m, "Post", url, contentType, body) | |||
| 437 | + | ret0, _ := ret[0].(ports.Response) | |||
| 438 | + | ret1, _ := ret[1].(error) | |||
| 439 | + | return ret0, ret1 | |||
| 440 | + | } | |||
| 441 | + | ||||
| 442 | + | // Post indicates an expected call of Post. | |||
| 443 | + | func (mr *MockHTTPClientMockRecorder) Post(url, contentType, body any) *gomock.Call { | |||
| 444 | + | mr.mock.ctrl.T.Helper() | |||
| 445 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockHTTPClient)(nil).Post), url, contentType, body) | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | // MockResponse is a mock of Response interface. | |||
| 449 | + | type MockResponse struct { | |||
| 450 | + | ctrl *gomock.Controller | |||
| 451 | + | recorder *MockResponseMockRecorder | |||
| 452 | + | isgomock struct{} | |||
| 453 | + | } | |||
| 454 | + | ||||
| 455 | + | // MockResponseMockRecorder is the mock recorder for MockResponse. | |||
| 456 | + | type MockResponseMockRecorder struct { | |||
| 457 | + | mock *MockResponse | |||
| 458 | + | } | |||
| 459 | + | ||||
| 460 | + | // NewMockResponse creates a new mock instance. | |||
| 461 | + | func NewMockResponse(ctrl *gomock.Controller) *MockResponse { | |||
| 462 | + | mock := &MockResponse{ctrl: ctrl} | |||
| 463 | + | mock.recorder = &MockResponseMockRecorder{mock} | |||
| 464 | + | return mock | |||
| 465 | + | } | |||
| 466 | + | ||||
| 467 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 468 | + | func (m *MockResponse) EXPECT() *MockResponseMockRecorder { | |||
| 469 | + | return m.recorder | |||
| 470 | + | } | |||
| 471 | + | ||||
| 472 | + | // Body mocks base method. | |||
| 473 | + | func (m *MockResponse) Body() []byte { | |||
| 474 | + | m.ctrl.T.Helper() | |||
| 475 | + | ret := m.ctrl.Call(m, "Body") | |||
| 476 | + | ret0, _ := ret[0].([]byte) | |||
| 477 | + | return ret0 | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | // Body indicates an expected call of Body. | |||
| 481 | + | func (mr *MockResponseMockRecorder) Body() *gomock.Call { | |||
| 482 | + | mr.mock.ctrl.T.Helper() | |||
| 483 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Body", reflect.TypeOf((*MockResponse)(nil).Body)) | |||
| 484 | + | } | |||
| 485 | + | ||||
| 486 | + | // Close mocks base method. | |||
| 487 | + | func (m *MockResponse) Close() error { | |||
| 488 | + | m.ctrl.T.Helper() | |||
| 489 | + | ret := m.ctrl.Call(m, "Close") | |||
| 490 | + | ret0, _ := ret[0].(error) | |||
| 491 | + | return ret0 | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | // Close indicates an expected call of Close. | |||
| 495 | + | func (mr *MockResponseMockRecorder) Close() *gomock.Call { | |||
| 496 | + | mr.mock.ctrl.T.Helper() | |||
| 497 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockResponse)(nil).Close)) | |||
| 498 | + | } | |||
| 499 | + | ||||
| 500 | + | // StatusCode mocks base method. | |||
| 501 | + | func (m *MockResponse) StatusCode() int { | |||
| 502 | + | m.ctrl.T.Helper() | |||
| 503 | + | ret := m.ctrl.Call(m, "StatusCode") | |||
| 504 | + | ret0, _ := ret[0].(int) | |||
| 505 | + | return ret0 | |||
| 506 | + | } | |||
| 507 | + | ||||
| 508 | + | // StatusCode indicates an expected call of StatusCode. | |||
| 509 | + | func (mr *MockResponseMockRecorder) StatusCode() *gomock.Call { | |||
| 510 | + | mr.mock.ctrl.T.Helper() | |||
| 511 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatusCode", reflect.TypeOf((*MockResponse)(nil).StatusCode)) | |||
| 512 | + | } | |||
| 513 | + | ||||
| 514 | + | // MockLogger is a mock of Logger interface. | |||
| 515 | + | type MockLogger struct { | |||
| 516 | + | ctrl *gomock.Controller | |||
| 517 | + | recorder *MockLoggerMockRecorder | |||
| 518 | + | isgomock struct{} | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | // MockLoggerMockRecorder is the mock recorder for MockLogger. | |||
| 522 | + | type MockLoggerMockRecorder struct { | |||
| 523 | + | mock *MockLogger | |||
| 524 | + | } | |||
| 525 | + | ||||
| 526 | + | // NewMockLogger creates a new mock instance. | |||
| 527 | + | func NewMockLogger(ctrl *gomock.Controller) *MockLogger { | |||
| 528 | + | mock := &MockLogger{ctrl: ctrl} | |||
| 529 | + | mock.recorder = &MockLoggerMockRecorder{mock} | |||
| 530 | + | return mock | |||
| 531 | + | } | |||
| 532 | + | ||||
| 533 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 534 | + | func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { | |||
| 535 | + | return m.recorder | |||
| 536 | + | } | |||
| 537 | + | ||||
| 538 | + | // Debug mocks base method. | |||
| 539 | + | func (m *MockLogger) Debug(msg string, keysAndValues ...any) { | |||
| 540 | + | m.ctrl.T.Helper() | |||
| 541 | + | varargs := []any{msg} | |||
| 542 | + | for _, a := range keysAndValues { | |||
| 543 | + | varargs = append(varargs, a) | |||
| 544 | + | } | |||
| 545 | + | m.ctrl.Call(m, "Debug", varargs...) | |||
| 546 | + | } | |||
| 547 | + | ||||
| 548 | + | // Debug indicates an expected call of Debug. | |||
| 549 | + | func (mr *MockLoggerMockRecorder) Debug(msg any, keysAndValues ...any) *gomock.Call { | |||
| 550 | + | mr.mock.ctrl.T.Helper() | |||
| 551 | + | varargs := append([]any{msg}, keysAndValues...) | |||
| 552 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) | |||
| 553 | + | } | |||
| 554 | + | ||||
| 555 | + | // Error mocks base method. | |||
| 556 | + | func (m *MockLogger) Error(msg string, keysAndValues ...any) { | |||
| 557 | + | m.ctrl.T.Helper() | |||
| 558 | + | varargs := []any{msg} | |||
| 559 | + | for _, a := range keysAndValues { | |||
| 560 | + | varargs = append(varargs, a) | |||
| 561 | + | } | |||
| 562 | + | m.ctrl.Call(m, "Error", varargs...) | |||
| 563 | + | } | |||
| 564 | + | ||||
| 565 | + | // Error indicates an expected call of Error. | |||
| 566 | + | func (mr *MockLoggerMockRecorder) Error(msg any, keysAndValues ...any) *gomock.Call { | |||
| 567 | + | mr.mock.ctrl.T.Helper() | |||
| 568 | + | varargs := append([]any{msg}, keysAndValues...) | |||
| 569 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) | |||
| 570 | + | } | |||
| 571 | + | ||||
| 572 | + | // Info mocks base method. | |||
| 573 | + | func (m *MockLogger) Info(msg string, keysAndValues ...any) { | |||
| 574 | + | m.ctrl.T.Helper() | |||
| 575 | + | varargs := []any{msg} | |||
| 576 | + | for _, a := range keysAndValues { | |||
| 577 | + | varargs = append(varargs, a) | |||
| 578 | + | } | |||
| 579 | + | m.ctrl.Call(m, "Info", varargs...) | |||
| 580 | + | } | |||
| 581 | + | ||||
| 582 | + | // Info indicates an expected call of Info. | |||
| 583 | + | func (mr *MockLoggerMockRecorder) Info(msg any, keysAndValues ...any) *gomock.Call { | |||
| 584 | + | mr.mock.ctrl.T.Helper() | |||
| 585 | + | varargs := append([]any{msg}, keysAndValues...) | |||
| 586 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) | |||
| 587 | + | } | |||
| 588 | + | ||||
| 589 | + | // LogPath mocks base method. | |||
| 590 | + | func (m *MockLogger) LogPath() string { | |||
| 591 | + | m.ctrl.T.Helper() | |||
| 592 | + | ret := m.ctrl.Call(m, "LogPath") | |||
| 593 | + | ret0, _ := ret[0].(string) | |||
| 594 | + | return ret0 | |||
| 595 | + | } | |||
| 596 | + | ||||
| 597 | + | // LogPath indicates an expected call of LogPath. | |||
| 598 | + | func (mr *MockLoggerMockRecorder) LogPath() *gomock.Call { | |||
| 599 | + | mr.mock.ctrl.T.Helper() | |||
| 600 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogPath", reflect.TypeOf((*MockLogger)(nil).LogPath)) | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | // Warn mocks base method. | |||
| 604 | + | func (m *MockLogger) Warn(msg string, keysAndValues ...any) { | |||
| 605 | + | m.ctrl.T.Helper() | |||
| 606 | + | varargs := []any{msg} | |||
| 607 | + | for _, a := range keysAndValues { | |||
| 608 | + | varargs = append(varargs, a) | |||
| 609 | + | } | |||
| 610 | + | m.ctrl.Call(m, "Warn", varargs...) | |||
| 611 | + | } | |||
| 612 | + | ||||
| 613 | + | // Warn indicates an expected call of Warn. | |||
| 614 | + | func (mr *MockLoggerMockRecorder) Warn(msg any, keysAndValues ...any) *gomock.Call { | |||
| 615 | + | mr.mock.ctrl.T.Helper() | |||
| 616 | + | varargs := append([]any{msg}, keysAndValues...) | |||
| 617 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) | |||
| 618 | + | } | |||
| 619 | + | ||||
| 620 | + | // MockInstallationRepository is a mock of InstallationRepository interface. | |||
| 621 | + | type MockInstallationRepository struct { | |||
| 622 | + | ctrl *gomock.Controller | |||
| 623 | + | recorder *MockInstallationRepositoryMockRecorder | |||
| 624 | + | isgomock struct{} | |||
| 625 | + | } | |||
| 626 | + | ||||
| 627 | + | // MockInstallationRepositoryMockRecorder is the mock recorder for MockInstallationRepository. | |||
| 628 | + | type MockInstallationRepositoryMockRecorder struct { | |||
| 629 | + | mock *MockInstallationRepository | |||
| 630 | + | } | |||
| 631 | + | ||||
| 632 | + | // NewMockInstallationRepository creates a new mock instance. | |||
| 633 | + | func NewMockInstallationRepository(ctrl *gomock.Controller) *MockInstallationRepository { | |||
| 634 | + | mock := &MockInstallationRepository{ctrl: ctrl} | |||
| 635 | + | mock.recorder = &MockInstallationRepositoryMockRecorder{mock} | |||
| 636 | + | return mock | |||
| 637 | + | } | |||
| 638 | + | ||||
| 639 | + | // EXPECT returns an object that allows the caller to indicate expected use. | |||
| 640 | + | func (m *MockInstallationRepository) EXPECT() *MockInstallationRepositoryMockRecorder { | |||
| 641 | + | return m.recorder | |||
| 642 | + | } | |||
| 643 | + | ||||
| 644 | + | // Exists mocks base method. | |||
| 645 | + | func (m *MockInstallationRepository) Exists(ctx context.Context, installationID string) (bool, error) { | |||
| 646 | + | m.ctrl.T.Helper() | |||
| 647 | + | ret := m.ctrl.Call(m, "Exists", ctx, installationID) | |||
| 648 | + | ret0, _ := ret[0].(bool) | |||
| 649 | + | ret1, _ := ret[1].(error) | |||
| 650 | + | return ret0, ret1 | |||
| 651 | + | } | |||
| 652 | + | ||||
| 653 | + | // Exists indicates an expected call of Exists. | |||
| 654 | + | func (mr *MockInstallationRepositoryMockRecorder) Exists(ctx, installationID any) *gomock.Call { | |||
| 655 | + | mr.mock.ctrl.T.Helper() | |||
| 656 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockInstallationRepository)(nil).Exists), ctx, installationID) | |||
| 657 | + | } | |||
| 658 | + | ||||
| 659 | + | // Load mocks base method. | |||
| 660 | + | func (m *MockInstallationRepository) Load(ctx context.Context, installationID string) (string, error) { | |||
| 661 | + | m.ctrl.T.Helper() | |||
| 662 | + | ret := m.ctrl.Call(m, "Load", ctx, installationID) | |||
| 663 | + | ret0, _ := ret[0].(string) | |||
| 664 | + | ret1, _ := ret[1].(error) | |||
| 665 | + | return ret0, ret1 | |||
| 666 | + | } | |||
| 667 | + | ||||
| 668 | + | // Load indicates an expected call of Load. | |||
| 669 | + | func (mr *MockInstallationRepositoryMockRecorder) Load(ctx, installationID any) *gomock.Call { | |||
| 670 | + | mr.mock.ctrl.T.Helper() | |||
| 671 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockInstallationRepository)(nil).Load), ctx, installationID) | |||
| 672 | + | } | |||
| 673 | + | ||||
| 674 | + | // Save mocks base method. | |||
| 675 | + | func (m *MockInstallationRepository) Save(ctx context.Context, installationID, state string) error { | |||
| 676 | + | m.ctrl.T.Helper() | |||
| 677 | + | ret := m.ctrl.Call(m, "Save", ctx, installationID, state) | |||
| 678 | + | ret0, _ := ret[0].(error) | |||
| 679 | + | return ret0 | |||
| 680 | + | } | |||
| 681 | + | ||||
| 682 | + | // Save indicates an expected call of Save. | |||
| 683 | + | func (mr *MockInstallationRepositoryMockRecorder) Save(ctx, installationID, state any) *gomock.Call { | |||
| 684 | + | mr.mock.ctrl.T.Helper() | |||
| 685 | + | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInstallationRepository)(nil).Save), ctx, installationID, state) | |||
| 686 | + | } | |||
M internal/domain/ports/ports.go +14 -0
| @@ -1,3 +1,5 @@ | |||
| 1 | + | //go:generate mockgen -destination=mocks/mock_ports.go -package=mocks . FileSystem,File,CommandExecutor,ChrootExecutor,ScriptExecutor,HTTPClient,Response,Logger,InstallationRepository | |
| 2 | + | ||
| 1 | 3 | package ports | |
| 2 | 4 | ||
| 3 | 5 | import ( | |
| @@ -107,3 +109,15 @@ type Logger interface { | |||
| 107 | 109 | // LogPath returns the path to the log file | |
| 108 | 110 | LogPath() string | |
| 109 | 111 | } | |
| 112 | + | ||
| 113 | + | // InstallationRepository is the port for installation persistence | |
| 114 | + | type InstallationRepository interface { | |
| 115 | + | // Save persists the installation state | |
| 116 | + | Save(ctx context.Context, installationID string, state string) error | |
| 117 | + | ||
| 118 | + | // Load retrieves the installation state | |
| 119 | + | Load(ctx context.Context, installationID string) (string, error) | |
| 120 | + | ||
| 121 | + | // Exists checks if an installation exists | |
| 122 | + | Exists(ctx context.Context, installationID string) (bool, error) | |
| 123 | + | } | |
| @@ -1,3 +1,5 @@ | |||||
| 1 | + | //go:generate mockgen -destination=mocks/mock_ports.go -package=mocks . FileSystem,File,CommandExecutor,ChrootExecutor,ScriptExecutor,HTTPClient,Response,Logger,InstallationRepository | |||
| 2 | + | ||||
| 1 | package ports | 3 | package ports | ||
| 2 | 4 | ||||
| 3 | import ( | 5 | import ( | ||
| @@ -107,3 +109,15 @@ type Logger interface { | |||||
| 107 | // LogPath returns the path to the log file | 109 | // LogPath returns the path to the log file | ||
| 108 | LogPath() string | 110 | LogPath() string | ||
| 109 | } | 111 | } | ||
| 112 | + | ||||
| 113 | + | // InstallationRepository is the port for installation persistence | |||
| 114 | + | type InstallationRepository interface { | |||
| 115 | + | // Save persists the installation state | |||
| 116 | + | Save(ctx context.Context, installationID string, state string) error | |||
| 117 | + | ||||
| 118 | + | // Load retrieves the installation state | |||
| 119 | + | Load(ctx context.Context, installationID string) (string, error) | |||
| 120 | + | ||||
| 121 | + | // Exists checks if an installation exists | |||
| 122 | + | Exists(ctx context.Context, installationID string) (bool, error) | |||
| 123 | + | } | |||