258 lines
8.8 KiB
PowerShell
258 lines
8.8 KiB
PowerShell
param(
|
|
[string]$TestCommand = "composer test:unit",
|
|
[ValidateSet("live", "debug")]
|
|
[string]$SourceTarget = "",
|
|
[string]$SourceHost = "",
|
|
[int]$SourcePort = 3306,
|
|
[string]$SourceDatabase = "",
|
|
[string]$SourceUser = "",
|
|
[string]$SourcePassword = "",
|
|
[string]$NetworkName = "api-test-net",
|
|
[string]$DbContainerName = "api-test-db",
|
|
[string]$RedisContainerName = "api-test-redis",
|
|
[string]$PhpImage = "api-php-test-runner",
|
|
[string]$CloneDatabase = "",
|
|
[string]$CloneRootPassword = "test_root_password",
|
|
[switch]$SkipBuild,
|
|
[switch]$ForceBuild,
|
|
[switch]$SkipTests,
|
|
[switch]$KeepContainers,
|
|
[switch]$Force
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Read-DotEnv {
|
|
param([string]$Path)
|
|
|
|
$values = @{}
|
|
if (-not (Test-Path $Path)) {
|
|
return $values
|
|
}
|
|
|
|
foreach ($line in Get-Content $Path) {
|
|
$trimmed = $line.Trim()
|
|
if ($trimmed -eq "" -or $trimmed.StartsWith("#")) {
|
|
continue
|
|
}
|
|
$idx = $trimmed.IndexOf("=")
|
|
if ($idx -lt 1) {
|
|
continue
|
|
}
|
|
$key = $trimmed.Substring(0, $idx).Trim()
|
|
$value = $trimmed.Substring($idx + 1).Trim()
|
|
if (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'"))) {
|
|
$value = $value.Substring(1, $value.Length - 2)
|
|
}
|
|
$values[$key] = $value
|
|
}
|
|
return $values
|
|
}
|
|
|
|
function Get-EnvOrDotEnv {
|
|
param(
|
|
[hashtable]$DotEnv,
|
|
[string]$Key
|
|
)
|
|
|
|
$envValue = [Environment]::GetEnvironmentVariable($Key)
|
|
if (-not [string]::IsNullOrWhiteSpace($envValue)) {
|
|
return $envValue
|
|
}
|
|
if ($DotEnv.ContainsKey($Key) -and -not [string]::IsNullOrWhiteSpace([string]$DotEnv[$Key])) {
|
|
return [string]$DotEnv[$Key]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function Resolve-DbValueByTarget {
|
|
param(
|
|
[hashtable]$DotEnv,
|
|
[string]$Target,
|
|
[string]$Suffix
|
|
)
|
|
|
|
$resolvedTarget = $Target
|
|
if ([string]::IsNullOrWhiteSpace($resolvedTarget)) {
|
|
$resolvedTarget = Get-EnvOrDotEnv -DotEnv $DotEnv -Key "CONFIG_DB_TARGET"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($resolvedTarget)) {
|
|
$resolvedTarget = "live"
|
|
}
|
|
$resolvedTarget = $resolvedTarget.ToLowerInvariant()
|
|
|
|
$liveKey = "CONFIG_DB_$Suffix"
|
|
$debugKey = "CONFIG_DB_DEBUG_$Suffix"
|
|
if ($resolvedTarget -eq "debug") {
|
|
$debugValue = Get-EnvOrDotEnv -DotEnv $DotEnv -Key $debugKey
|
|
if (-not [string]::IsNullOrWhiteSpace($debugValue)) {
|
|
return $debugValue
|
|
}
|
|
}
|
|
|
|
return Get-EnvOrDotEnv -DotEnv $DotEnv -Key $liveKey
|
|
}
|
|
|
|
function Remove-ContainerIfExists {
|
|
param([string]$Name)
|
|
|
|
$existingRaw = & docker ps -aq --filter "name=^${Name}$"
|
|
$existing = (($existingRaw | Out-String).Trim())
|
|
if (-not [string]::IsNullOrWhiteSpace($existing)) {
|
|
& docker rm -f $Name | Out-Null
|
|
}
|
|
}
|
|
|
|
$RootDir = Split-Path -Parent $PSScriptRoot
|
|
Set-Location $RootDir
|
|
|
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
|
throw "Docker is required but was not found in PATH."
|
|
}
|
|
|
|
$dotEnv = Read-DotEnv -Path (Join-Path $RootDir ".env")
|
|
|
|
if ([string]::IsNullOrWhiteSpace($SourceTarget)) {
|
|
$SourceTarget = (Get-EnvOrDotEnv -DotEnv $dotEnv -Key "CONFIG_DB_TARGET").ToLowerInvariant()
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($SourceTarget)) {
|
|
$SourceTarget = "live"
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($SourceHost)) {
|
|
$SourceHost = Resolve-DbValueByTarget -DotEnv $dotEnv -Target $SourceTarget -Suffix "HOST"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($SourceDatabase)) {
|
|
$SourceDatabase = Resolve-DbValueByTarget -DotEnv $dotEnv -Target $SourceTarget -Suffix "DATABASE"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($SourceUser)) {
|
|
$SourceUser = Resolve-DbValueByTarget -DotEnv $dotEnv -Target $SourceTarget -Suffix "USER"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($SourcePassword)) {
|
|
$SourcePassword = Resolve-DbValueByTarget -DotEnv $dotEnv -Target $SourceTarget -Suffix "PASSWORD"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($CloneDatabase)) {
|
|
$CloneDatabase = "${SourceDatabase}_test_clone"
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($SourceHost) -or [string]::IsNullOrWhiteSpace($SourceDatabase) -or [string]::IsNullOrWhiteSpace($SourceUser) -or [string]::IsNullOrWhiteSpace($SourcePassword)) {
|
|
throw "Missing source DB credentials for target '$SourceTarget'. Set CONFIG_DB_TARGET and CONFIG_DB_* / CONFIG_DB_DEBUG_* in .env or pass script parameters."
|
|
}
|
|
|
|
if (-not $Force) {
|
|
Write-Host "This will clone data from the '$SourceTarget' database target into a local Docker MySQL container." -ForegroundColor Yellow
|
|
$confirmation = Read-Host "Type CLONE to continue"
|
|
if ($confirmation -ne "CLONE") {
|
|
throw "Cancelled."
|
|
}
|
|
}
|
|
|
|
if (-not $SkipBuild) {
|
|
& docker image inspect $PhpImage *> $null
|
|
$imageExists = ($LASTEXITCODE -eq 0)
|
|
if ($imageExists -and -not $ForceBuild) {
|
|
Write-Host "Using cached PHP test image ($PhpImage). Use -ForceBuild to rebuild." -ForegroundColor Cyan
|
|
} else {
|
|
Write-Host "Building PHP test image ($PhpImage)..." -ForegroundColor Cyan
|
|
& docker build -f services/php/Dockerfile -t $PhpImage .
|
|
}
|
|
}
|
|
|
|
$networkExists = (& docker network ls --format "{{.Name}}" | Where-Object { $_ -eq $NetworkName })
|
|
if (-not $networkExists) {
|
|
Write-Host "Creating docker network $NetworkName..." -ForegroundColor Cyan
|
|
& docker network create $NetworkName | Out-Null
|
|
}
|
|
|
|
Write-Host "Resetting test containers..." -ForegroundColor Cyan
|
|
Remove-ContainerIfExists -Name $DbContainerName
|
|
Remove-ContainerIfExists -Name $RedisContainerName
|
|
|
|
Write-Host "Starting cloned MySQL container ($DbContainerName)..." -ForegroundColor Cyan
|
|
& docker run -d `
|
|
--name $DbContainerName `
|
|
--network $NetworkName `
|
|
-e "MYSQL_ROOT_PASSWORD=$CloneRootPassword" `
|
|
-e "MYSQL_DATABASE=$CloneDatabase" `
|
|
mysql:8.4 | Out-Null
|
|
|
|
$maxAttempts = 60
|
|
for ($i = 1; $i -le $maxAttempts; $i++) {
|
|
& docker exec -e "MYSQL_PWD=$CloneRootPassword" $DbContainerName sh -lc "mysqladmin -u root ping --silent" *> $null
|
|
if ($LASTEXITCODE -eq 0) {
|
|
break
|
|
}
|
|
if ($i -eq $maxAttempts) {
|
|
throw "Timed out waiting for $DbContainerName to be ready."
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
|
|
Write-Host "Cloning '$SourceTarget' database $SourceDatabase from $SourceHost into $DbContainerName/$CloneDatabase..." -ForegroundColor Cyan
|
|
$dumpDir = Join-Path $RootDir ".tmp-db-clone"
|
|
$dumpFile = Join-Path $dumpDir "dump.sql"
|
|
if (-not (Test-Path $dumpDir)) {
|
|
New-Item -Path $dumpDir -ItemType Directory | Out-Null
|
|
}
|
|
if (Test-Path $dumpFile) {
|
|
Remove-Item -Path $dumpFile -Force
|
|
}
|
|
|
|
$dumpMount = $dumpDir.Replace('\', '/')
|
|
$dumpCmd = "exec mysqldump --compress --single-transaction --quick --set-gtid-purged=OFF -h '$SourceHost' -P '$SourcePort' -u '$SourceUser' '$SourceDatabase' > /dump/dump.sql"
|
|
& docker run --rm `
|
|
-e "MYSQL_PWD=$SourcePassword" `
|
|
-v "${dumpMount}:/dump" `
|
|
mysql:8.4 sh -lc $dumpCmd
|
|
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $dumpFile)) {
|
|
throw "Database dump failed."
|
|
}
|
|
|
|
$importCmd = "exec mysql -h '$DbContainerName' -u root '$CloneDatabase' < /dump/dump.sql"
|
|
& docker run --rm `
|
|
--network $NetworkName `
|
|
-e "MYSQL_PWD=$CloneRootPassword" `
|
|
-v "${dumpMount}:/dump" `
|
|
mysql:8.4 sh -lc $importCmd
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Database import failed."
|
|
}
|
|
|
|
Remove-Item -Path $dumpFile -Force -ErrorAction SilentlyContinue
|
|
|
|
Write-Host "Starting isolated Redis container ($RedisContainerName)..." -ForegroundColor Cyan
|
|
& docker run -d --name $RedisContainerName --network $NetworkName redis:7 | Out-Null
|
|
|
|
if (-not $SkipTests) {
|
|
Write-Host "Running tests in a separate PHP container against cloned DB..." -ForegroundColor Cyan
|
|
$appPath = Join-Path $RootDir "services/nginx/app"
|
|
$phpIniPath = Join-Path $RootDir "services/php/php.ini"
|
|
|
|
& docker run --rm `
|
|
--network $NetworkName `
|
|
--env-file (Join-Path $RootDir ".env") `
|
|
-e "CONFIG_DB_HOST=$DbContainerName" `
|
|
-e "CONFIG_DB_DATABASE=$CloneDatabase" `
|
|
-e "CONFIG_DB_USER=root" `
|
|
-e "CONFIG_DB_PASSWORD=$CloneRootPassword" `
|
|
-e "REDIS_CONFIG_HOST=$RedisContainerName" `
|
|
-e "AUTO_COMPOSER_INSTALL=false" `
|
|
-v "${appPath}:/var/www/html" `
|
|
-v "${phpIniPath}:/usr/local/etc/php/conf.d/zz-custom.ini:ro" `
|
|
$PhpImage `
|
|
sh -lc "cd /var/www/html && if [ ! -f vendor/autoload.php ]; then composer install --no-interaction --prefer-dist; fi && $TestCommand"
|
|
}
|
|
|
|
if (-not $KeepContainers) {
|
|
Write-Host "Cleaning up test containers..." -ForegroundColor Cyan
|
|
Remove-ContainerIfExists -Name $RedisContainerName
|
|
Remove-ContainerIfExists -Name $DbContainerName
|
|
} else {
|
|
Write-Host "Keeping containers for inspection:" -ForegroundColor Yellow
|
|
Write-Host " DB: $DbContainerName"
|
|
Write-Host " Redis: $RedisContainerName"
|
|
}
|
|
|
|
Write-Host "Done." -ForegroundColor Green
|