Improve live DB clone speed
This commit is contained in:
@@ -18,3 +18,22 @@ LETSENCRYPT_PATH=/etc/letsencrypt
|
||||
# - If you do not have certs for cloud.truckwash.dk locally, either comment out that
|
||||
# TLS server block in services/nginx/nginx.conf or place a temporary self-signed
|
||||
# cert/key pair at the expected path.
|
||||
|
||||
# Database target selection used by services/nginx/app/config.php
|
||||
# Allowed values: live, debug
|
||||
CONFIG_DB_TARGET=live
|
||||
|
||||
# Live DB credentials
|
||||
CONFIG_DB_HOST=
|
||||
CONFIG_DB_USER=
|
||||
CONFIG_DB_PASSWORD=
|
||||
CONFIG_DB_DATABASE=
|
||||
CONFIG_DB_SSL_MODE=DISABLED
|
||||
|
||||
# Debug DB credentials (used when CONFIG_DB_TARGET=debug)
|
||||
# Any blank debug value falls back to the live value above.
|
||||
CONFIG_DB_DEBUG_HOST=
|
||||
CONFIG_DB_DEBUG_USER=
|
||||
CONFIG_DB_DEBUG_PASSWORD=
|
||||
CONFIG_DB_DEBUG_DATABASE=
|
||||
CONFIG_DB_DEBUG_SSL_MODE=DISABLED
|
||||
|
||||
@@ -58,6 +58,28 @@ $env:RUN_INTEGRATION_TESTS='1'
|
||||
composer test:integration
|
||||
```
|
||||
|
||||
### Run Tests Against A Cloned Live DB (Docker-Isolated)
|
||||
Use the helper scripts in `scripts/` to:
|
||||
1. Clone the configured live DB into a local MySQL Docker container.
|
||||
2. Start an isolated Redis container.
|
||||
3. Run tests in a separate temporary PHP Docker container pointed at that clone.
|
||||
|
||||
PowerShell:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\clone-live-db-and-test.ps1 -Force
|
||||
```
|
||||
|
||||
Bash:
|
||||
```bash
|
||||
FORCE=1 ./scripts/clone-live-db-and-test.sh
|
||||
```
|
||||
|
||||
Optional overrides:
|
||||
- Test command: `-TestCommand "composer test:integration"` or `TEST_COMMAND="composer test:integration"`
|
||||
- Keep containers after run: `-KeepContainers` or `KEEP_CONTAINERS=1`
|
||||
- Skip image build: `-SkipBuild` or `SKIP_BUILD=1`
|
||||
- Force image rebuild: `-ForceBuild` or `FORCE_BUILD=1`
|
||||
|
||||
### Test Layout
|
||||
- `tests/Unit/*`: isolated unit and route-level behavior tests.
|
||||
- `tests/Integration/*`: Redis/DB-backed tests intended for Docker/CI environments.
|
||||
|
||||
+25
-7
@@ -68,15 +68,33 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') {
|
||||
'REDIS_CONFIG_DATABASE' => 'database',
|
||||
'REDIS_CONFIG_PASSWORD' => 'password'
|
||||
];
|
||||
$dbTarget = strtolower(trim((string)($_ENV['CONFIG_DB_TARGET'] ?? 'live')));
|
||||
if ($dbTarget !== 'live' && $dbTarget !== 'debug') {
|
||||
$dbTarget = 'live';
|
||||
}
|
||||
|
||||
$resolveDbValue = function (string $key) use ($dbTarget): string {
|
||||
$liveKey = 'CONFIG_DB_' . $key;
|
||||
$debugKey = 'CONFIG_DB_DEBUG_' . $key;
|
||||
$liveValue = (string)($_ENV[$liveKey] ?? '');
|
||||
$debugValue = (string)($_ENV[$debugKey] ?? '');
|
||||
|
||||
if ($dbTarget === 'debug' && $debugValue !== '') {
|
||||
return $debugValue;
|
||||
}
|
||||
|
||||
return $liveValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the db configuration
|
||||
* Set the db configuration from selected target (live/debug)
|
||||
*/
|
||||
$CONFIG_DB = [
|
||||
'host' => $_ENV['CONFIG_DB_HOST'],
|
||||
'user' => $_ENV['CONFIG_DB_USER'],
|
||||
'password' => $_ENV['CONFIG_DB_PASSWORD'],
|
||||
'database' => $_ENV['CONFIG_DB_DATABASE'],
|
||||
'ssl_mode' => $_ENV['CONFIG_DB_SSL_MODE'] ?? 'DISABLED'
|
||||
'host' => $resolveDbValue('HOST'),
|
||||
'user' => $resolveDbValue('USER'),
|
||||
'password' => $resolveDbValue('PASSWORD'),
|
||||
'database' => $resolveDbValue('DATABASE'),
|
||||
'ssl_mode' => $resolveDbValue('SSL_MODE') !== '' ? $resolveDbValue('SSL_MODE') : 'DISABLED'
|
||||
];
|
||||
/**
|
||||
* Set the debug configuration
|
||||
@@ -134,4 +152,4 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') {
|
||||
// Set the timezone
|
||||
date_default_timezone_set($_ENV['CONFIG_TIMEZONE']) ?? 'Europe/Copenhagen';
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
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
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
TEST_COMMAND="${TEST_COMMAND:-composer test:unit}"
|
||||
SOURCE_TARGET="${SOURCE_TARGET:-}"
|
||||
SOURCE_HOST="${SOURCE_HOST:-${CONFIG_DB_HOST:-}}"
|
||||
SOURCE_PORT="${SOURCE_PORT:-${CONFIG_DB_PORT:-3306}}"
|
||||
SOURCE_DATABASE="${SOURCE_DATABASE:-${CONFIG_DB_DATABASE:-}}"
|
||||
SOURCE_USER="${SOURCE_USER:-${CONFIG_DB_USER:-}}"
|
||||
SOURCE_PASSWORD="${SOURCE_PASSWORD:-${CONFIG_DB_PASSWORD:-}}"
|
||||
NETWORK_NAME="${NETWORK_NAME:-api-test-net}"
|
||||
DB_CONTAINER_NAME="${DB_CONTAINER_NAME:-api-test-db}"
|
||||
REDIS_CONTAINER_NAME="${REDIS_CONTAINER_NAME:-api-test-redis}"
|
||||
PHP_IMAGE="${PHP_IMAGE:-api-php-test-runner}"
|
||||
CLONE_DATABASE="${CLONE_DATABASE:-}"
|
||||
CLONE_ROOT_PASSWORD="${CLONE_ROOT_PASSWORD:-test_root_password}"
|
||||
SKIP_BUILD="${SKIP_BUILD:-0}"
|
||||
FORCE_BUILD="${FORCE_BUILD:-0}"
|
||||
SKIP_TESTS="${SKIP_TESTS:-0}"
|
||||
KEEP_CONTAINERS="${KEEP_CONTAINERS:-0}"
|
||||
FORCE="${FORCE:-0}"
|
||||
|
||||
if [[ -f ".env" ]]; then
|
||||
# shellcheck disable=SC2046
|
||||
export $(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' .env | sed 's/\r$//')
|
||||
SOURCE_TARGET="${SOURCE_TARGET:-${CONFIG_DB_TARGET:-}}"
|
||||
fi
|
||||
|
||||
SOURCE_TARGET="${SOURCE_TARGET:-live}"
|
||||
SOURCE_TARGET="$(echo "${SOURCE_TARGET}" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "${SOURCE_TARGET}" != "live" && "${SOURCE_TARGET}" != "debug" ]]; then
|
||||
SOURCE_TARGET="live"
|
||||
fi
|
||||
|
||||
if [[ "${SOURCE_TARGET}" == "debug" ]]; then
|
||||
SOURCE_HOST="${SOURCE_HOST:-${CONFIG_DB_DEBUG_HOST:-${CONFIG_DB_HOST:-}}}"
|
||||
SOURCE_DATABASE="${SOURCE_DATABASE:-${CONFIG_DB_DEBUG_DATABASE:-${CONFIG_DB_DATABASE:-}}}"
|
||||
SOURCE_USER="${SOURCE_USER:-${CONFIG_DB_DEBUG_USER:-${CONFIG_DB_USER:-}}}"
|
||||
SOURCE_PASSWORD="${SOURCE_PASSWORD:-${CONFIG_DB_DEBUG_PASSWORD:-${CONFIG_DB_PASSWORD:-}}}"
|
||||
else
|
||||
SOURCE_HOST="${SOURCE_HOST:-${CONFIG_DB_HOST:-}}"
|
||||
SOURCE_DATABASE="${SOURCE_DATABASE:-${CONFIG_DB_DATABASE:-}}"
|
||||
SOURCE_USER="${SOURCE_USER:-${CONFIG_DB_USER:-}}"
|
||||
SOURCE_PASSWORD="${SOURCE_PASSWORD:-${CONFIG_DB_PASSWORD:-}}"
|
||||
fi
|
||||
|
||||
if [[ -z "${SOURCE_HOST}" || -z "${SOURCE_DATABASE}" || -z "${SOURCE_USER}" || -z "${SOURCE_PASSWORD}" ]]; then
|
||||
echo "Missing source DB credentials for target '${SOURCE_TARGET}'. Set CONFIG_DB_TARGET and CONFIG_DB_* / CONFIG_DB_DEBUG_* in .env or env vars."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${CLONE_DATABASE}" ]]; then
|
||||
CLONE_DATABASE="${SOURCE_DATABASE}_test_clone"
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker is required but was not found in PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${FORCE}" != "1" ]]; then
|
||||
echo "This will clone data from the '${SOURCE_TARGET}' database target into a local Docker MySQL container."
|
||||
read -r -p "Type CLONE to continue: " confirmation
|
||||
if [[ "${confirmation}" != "CLONE" ]]; then
|
||||
echo "Cancelled."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
remove_container_if_exists() {
|
||||
local name="$1"
|
||||
if docker ps -aq --filter "name=^${name}$" | grep -q .; then
|
||||
docker rm -f "${name}" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${SKIP_BUILD}" != "1" ]]; then
|
||||
if docker image inspect "${PHP_IMAGE}" >/dev/null 2>&1 && [[ "${FORCE_BUILD}" != "1" ]]; then
|
||||
echo "Using cached PHP test image (${PHP_IMAGE}). Set FORCE_BUILD=1 to rebuild."
|
||||
else
|
||||
echo "Building PHP test image (${PHP_IMAGE})..."
|
||||
docker build -f services/php/Dockerfile -t "${PHP_IMAGE}" .
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! docker network ls --format '{{.Name}}' | grep -qx "${NETWORK_NAME}"; then
|
||||
echo "Creating docker network ${NETWORK_NAME}..."
|
||||
docker network create "${NETWORK_NAME}" >/dev/null
|
||||
fi
|
||||
|
||||
echo "Resetting test containers..."
|
||||
remove_container_if_exists "${DB_CONTAINER_NAME}"
|
||||
remove_container_if_exists "${REDIS_CONTAINER_NAME}"
|
||||
|
||||
echo "Starting cloned MySQL container (${DB_CONTAINER_NAME})..."
|
||||
docker run -d \
|
||||
--name "${DB_CONTAINER_NAME}" \
|
||||
--network "${NETWORK_NAME}" \
|
||||
-e "MYSQL_ROOT_PASSWORD=${CLONE_ROOT_PASSWORD}" \
|
||||
-e "MYSQL_DATABASE=${CLONE_DATABASE}" \
|
||||
mysql:8.4 >/dev/null
|
||||
|
||||
for i in $(seq 1 60); do
|
||||
if docker exec -e "MYSQL_PWD=${CLONE_ROOT_PASSWORD}" "${DB_CONTAINER_NAME}" sh -lc "mysqladmin -u root ping --silent" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
if [[ "$i" -eq 60 ]]; then
|
||||
echo "Timed out waiting for ${DB_CONTAINER_NAME} to be ready."
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Cloning '${SOURCE_TARGET}' database ${SOURCE_DATABASE} from ${SOURCE_HOST} into ${DB_CONTAINER_NAME}/${CLONE_DATABASE}..."
|
||||
DUMP_DIR="${ROOT_DIR}/.tmp-db-clone"
|
||||
DUMP_FILE="${DUMP_DIR}/dump.sql"
|
||||
mkdir -p "${DUMP_DIR}"
|
||||
rm -f "${DUMP_FILE}"
|
||||
|
||||
docker run --rm \
|
||||
-e "MYSQL_PWD=${SOURCE_PASSWORD}" \
|
||||
-v "${DUMP_DIR}:/dump" \
|
||||
mysql:8.4 \
|
||||
sh -lc "exec mysqldump --compress --single-transaction --quick --set-gtid-purged=OFF -h '${SOURCE_HOST}' -P '${SOURCE_PORT}' -u '${SOURCE_USER}' '${SOURCE_DATABASE}' > /dump/dump.sql"
|
||||
|
||||
if [[ ! -f "${DUMP_FILE}" ]]; then
|
||||
echo "Database dump failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run --rm \
|
||||
--network "${NETWORK_NAME}" \
|
||||
-e "MYSQL_PWD=${CLONE_ROOT_PASSWORD}" \
|
||||
-v "${DUMP_DIR}:/dump" \
|
||||
mysql:8.4 \
|
||||
sh -lc "exec mysql -h '${DB_CONTAINER_NAME}' -u root '${CLONE_DATABASE}' < /dump/dump.sql"
|
||||
|
||||
rm -f "${DUMP_FILE}"
|
||||
|
||||
echo "Starting isolated Redis container (${REDIS_CONTAINER_NAME})..."
|
||||
docker run -d --name "${REDIS_CONTAINER_NAME}" --network "${NETWORK_NAME}" redis:7 >/dev/null
|
||||
|
||||
if [[ "${SKIP_TESTS}" != "1" ]]; then
|
||||
echo "Running tests in a separate PHP container against cloned DB..."
|
||||
docker run --rm \
|
||||
--network "${NETWORK_NAME}" \
|
||||
--env-file "${ROOT_DIR}/.env" \
|
||||
-e "CONFIG_DB_HOST=${DB_CONTAINER_NAME}" \
|
||||
-e "CONFIG_DB_DATABASE=${CLONE_DATABASE}" \
|
||||
-e "CONFIG_DB_USER=root" \
|
||||
-e "CONFIG_DB_PASSWORD=${CLONE_ROOT_PASSWORD}" \
|
||||
-e "REDIS_CONFIG_HOST=${REDIS_CONTAINER_NAME}" \
|
||||
-e "AUTO_COMPOSER_INSTALL=false" \
|
||||
-v "${ROOT_DIR}/services/nginx/app:/var/www/html" \
|
||||
-v "${ROOT_DIR}/services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro" \
|
||||
"${PHP_IMAGE}" \
|
||||
sh -lc "cd /var/www/html && if [ ! -f vendor/autoload.php ]; then composer install --no-interaction --prefer-dist; fi && ${TEST_COMMAND}"
|
||||
fi
|
||||
|
||||
if [[ "${KEEP_CONTAINERS}" != "1" ]]; then
|
||||
echo "Cleaning up test containers..."
|
||||
remove_container_if_exists "${REDIS_CONTAINER_NAME}"
|
||||
remove_container_if_exists "${DB_CONTAINER_NAME}"
|
||||
else
|
||||
echo "Keeping containers for inspection:"
|
||||
echo " DB: ${DB_CONTAINER_NAME}"
|
||||
echo " Redis: ${REDIS_CONTAINER_NAME}"
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[ValidateSet("start", "stop", "logs", "test")]
|
||||
[string]$Action = "start",
|
||||
[string]$Service = "php1"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RootDir = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $RootDir
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"pest_3.8.6","defects":[],"times":{"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_creates_a_64_char_auth_token_for_an_existing_user":0.131,"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_throws_a_clear_exception_when_customer_user_is_missing":0.025,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_passkey_challenge_legacy_script_green_during_migration":0.037,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_register_CVR_legacy_script_green_during_migration":0.033,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_deterministic_department_digit_map_and_caps_to_9_options":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_department_ids_by_selected_digit":0.008,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_gate_type_digit_to_entrance_or_exit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_stores__loads_and_clears_ivr_state_with_ttl":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_validates_state_caller_fingerprint_matching":0.018,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_department_and_gate_prompts":0.009,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_creates_permission_nodes_linked_to_subuser_permission_keys":0.01,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_rejects_empty_permission_definitions":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_button_arrays__json_and_csv_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_button_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_vehicle_type_values_and_null_semantics":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_vehicle_type_values":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_non_numeric_vehicle_type_strings":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_keeps_lane_service_enum_contract_for_MACHINE":0.021,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_booking_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_selfserve_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubusersRoutePermissionLinkTest::__pest_evaluable_it_keeps_subusers_route_list_permission_linked_to_SUBUSERS__LIST_node":0.006}}
|
||||
@@ -25,10 +25,22 @@
|
||||
"spipu/html2pdf": "^5.3",
|
||||
"web-auth/webauthn-lib": "^5.2"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"classes/",
|
||||
"interfaces/",
|
||||
"traits/",
|
||||
"objects/",
|
||||
"modules/",
|
||||
"routes/",
|
||||
"statistics/"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"tbachert/spi": true
|
||||
"tbachert/spi": true,
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2962
-13
File diff suppressed because it is too large
Load Diff
@@ -27,14 +27,32 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') {
|
||||
'REDIS_CONFIG_DATABASE' => 'database',
|
||||
'REDIS_CONFIG_PASSWORD' => 'password'
|
||||
];
|
||||
$dbTarget = strtolower(trim((string)($_ENV['CONFIG_DB_TARGET'] ?? 'live')));
|
||||
if ($dbTarget !== 'live' && $dbTarget !== 'debug') {
|
||||
$dbTarget = 'live';
|
||||
}
|
||||
|
||||
$resolveDbValue = function (string $key) use ($dbTarget): string {
|
||||
$liveKey = 'CONFIG_DB_' . $key;
|
||||
$debugKey = 'CONFIG_DB_DEBUG_' . $key;
|
||||
$liveValue = (string)($_ENV[$liveKey] ?? '');
|
||||
$debugValue = (string)($_ENV[$debugKey] ?? '');
|
||||
|
||||
if ($dbTarget === 'debug' && $debugValue !== '') {
|
||||
return $debugValue;
|
||||
}
|
||||
|
||||
return $liveValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the db configuration
|
||||
* Set the db configuration from selected target (live/debug)
|
||||
*/
|
||||
$CONFIG_DB = [
|
||||
'host' => $_ENV['CONFIG_DB_HOST'],
|
||||
'user' => $_ENV['CONFIG_DB_USER'],
|
||||
'password' => $_ENV['CONFIG_DB_PASSWORD'],
|
||||
'database' => $_ENV['CONFIG_DB_DATABASE']
|
||||
'host' => $resolveDbValue('HOST'),
|
||||
'user' => $resolveDbValue('USER'),
|
||||
'password' => $resolveDbValue('PASSWORD'),
|
||||
'database' => $resolveDbValue('DATABASE')
|
||||
];
|
||||
/**
|
||||
* Set the debug configuration
|
||||
@@ -98,4 +116,4 @@ if (isset($_ENV['USE_ENV']) && $_ENV['USE_ENV'] === 'true') {
|
||||
} else {
|
||||
// Throw an error if the environment variables are not set
|
||||
throw new Exception('Environment variables are not set');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,11 @@ class authRoute
|
||||
'pageSize' => 1, // Since the limit is 1000, we need to set the page size to 1000.
|
||||
])->collection);
|
||||
// Check if the customer number is already in use in our system
|
||||
if ((new users_o())->getUserByCustomerNumber((int)$companyPhone)->exists()) {
|
||||
$existingCompanyUser = (new users_o())->getUserByCustomerNumber((int)$companyPhone);
|
||||
$companyPhoneAlreadyRegistered = method_exists($existingCompanyUser, 'exists')
|
||||
? (bool)$existingCompanyUser->exists()
|
||||
: ((int)($existingCompanyUser->id ?? 0) > 0);
|
||||
if ($companyPhoneAlreadyRegistered) {
|
||||
$response->error('Company phone number already registered', 400);
|
||||
}
|
||||
if (count($economic_response) === 0) {
|
||||
@@ -584,7 +588,10 @@ class authRoute
|
||||
$challenge = rtrim(strtr(base64_encode($rawChallenge), '+/', '-_'), '=');
|
||||
|
||||
// rpId for passkeys
|
||||
$rpId = parse_url((string)$_SERVER['HTTP_ORIGIN'], PHP_URL_HOST) ?: $_SERVER['SERVER_NAME'];
|
||||
$originHost = parse_url((string)($_SERVER['HTTP_ORIGIN'] ?? ''), PHP_URL_HOST);
|
||||
$httpHost = isset($_SERVER['HTTP_HOST']) ? explode(':', (string)$_SERVER['HTTP_HOST'])[0] : null;
|
||||
$serverName = $_SERVER['SERVER_NAME'] ?? null;
|
||||
$rpId = $originHost ?: $httpHost ?: $serverName ?: 'truckwash.io';
|
||||
|
||||
// Create an ephemeral token to bind the challenge to the (potential) user
|
||||
(new tokens_o())->create($user_id, $challenge_token, 'PASSKEY_CHALLENGE');
|
||||
@@ -691,4 +698,4 @@ class authRoute
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ it('builds deterministic department digit map and caps to 9 options', function (
|
||||
$map = $route->mapDepartments([11, 22, 0, -5, 33, 44, 55, 66, 77, 88, 99]);
|
||||
|
||||
expect($map)->toHaveCount(9);
|
||||
expect(array_keys($map))->toBe(['1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||
expect(array_keys($map))->toBe([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
expect($map['1']['department_id'])->toBe(11);
|
||||
expect($map['2']['department_id'])->toBe(22);
|
||||
expect($map['3']['department_id'])->toBe(33);
|
||||
|
||||
Reference in New Issue
Block a user