Improve live DB clone speed

This commit is contained in:
Jeppe Bundgaard
2026-03-12 13:26:22 +01:00
parent e91dda0799
commit 5cb197898d
12 changed files with 3510 additions and 34 deletions
+19
View File
@@ -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
+22
View File
@@ -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
View File
@@ -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';
}
}
+257
View File
@@ -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
+173
View File
@@ -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
View File
@@ -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}}
+13 -1
View File
@@ -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
}
}
}
+2962 -13
View File
File diff suppressed because it is too large Load Diff
+24 -6
View File
@@ -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');
}
}
+10 -3
View File
@@ -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);