name: PHPUnit Tests on: push: paths: - '**.php' - 'spark' - 'tests/**' - '.github/workflows/phpunit.yml' - 'gulpfile.js' - 'app/Database/**' pull_request: paths: - '**.php' - 'spark' - 'tests/**' - '.github/workflows/phpunit.yml' - 'gulpfile.js' - 'app/Database/**' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true permissions: contents: read jobs: test: name: PHP ${{ matrix.php-version }} / ${{ matrix.db-image }} Tests runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: php-version: - '8.2' - '8.3' - '8.4' db-image: - 'mariadb:10.5' - 'mariadb:10.6' - 'mariadb:10.11' - 'mysql:5.7' - 'mysql:8.0' - 'mysql:8.4' steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} extensions: intl, mbstring, mysqli coverage: none - name: Verify PHP patch level run: | ACTUAL=$(php -r 'echo PHP_VERSION;') case "${ACTUAL%.*}" in 8.2) MIN="8.2.35" ;; 8.3) MIN="8.3.36" ;; 8.4) MIN="8.4.27" ;; 8.5) MIN="8.5.12" ;; *) echo "::error::Unsupported PHP branch: $ACTUAL"; exit 1 ;; esac if [ "$(printf '%s\n' "$MIN" "$ACTUAL" | sort -V | head -1)" != "$MIN" ]; then echo "::error::PHP $ACTUAL is below patched minimum $MIN (CVE-2026-109618 / CVE-2026-109619)" exit 1 fi echo "PHP $ACTUAL meets minimum $MIN" - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Get npm cache directory run: echo "NPM_CACHE_DIR=$(npm config get cache)" >> $GITHUB_ENV - name: Cache npm dependencies uses: actions/cache@v3 with: path: ${{ env.NPM_CACHE_DIR }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install npm dependencies run: npm install - name: Start database run: | docker run -d --name mysql \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=ospos_test \ -e MYSQL_USER=admin \ -e MYSQL_PASSWORD=pointofsale \ -p 3306:3306 \ ${{ matrix.db-image }} # Wait for database to be ready (max 60 attempts) retries=0 until docker exec mysql mysqladmin ping -h 127.0.0.1 -u root -proot --silent; do retries=$((retries + 1)) if [ $retries -ge 60 ]; then echo "Database failed to start after 60 attempts" exit 1 fi echo "Waiting for database... (attempt $retries/60)" sleep 2 done echo "Database is ready!" # Grant admin the CREATE/DROP privileges it needs to drop and # recreate the test database at the start of each test class. # Scoped to ospos_test only — never global *.* — so a compromised # test process cannot alter or drop unrelated schemas. docker exec mysql mysql -u root -proot \ --execute="GRANT CREATE, DROP ON ospos_test.* TO 'admin'@'%'; FLUSH PRIVILEGES;" - name: Get composer cache directory run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies uses: actions/cache@v3 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-${{ matrix.php-version }}- ${{ runner.os }}- - name: Install dependencies run: composer update --ansi --no-interaction - name: Create .env file run: cp .env.example .env - name: Provision per-run encryption key # Generates a unique key for this run and exports it as a real OS env # var (ENCRYPTION_KEY), which the app's Encryption config reads as a # fallback after the normal .env lookup. This is supported explicitly, # so no shared key is committed or shipped. run: | KEY=$(openssl rand -hex 32) printf 'ENCRYPTION_KEY=%s\n' "$KEY" >> "$GITHUB_ENV" - name: Provision per-run throttle key # checkThrottleEncryption() is a read-only guard that throws unless a # throttle.key is present (app/Helpers/security_helper.php). It is # normally minted at container startup via `php spark env:provision`, # so the test environment must provide one. # # We write it into the .env file (replacing the empty placeholder # copied from .env.example) rather than exporting an OS env var: # CodeIgniter's env() resolves $_ENV first, and DotEnv populates # $_ENV['throttle.key']='' from .env, which would shadow any OS var # before getenv() is ever consulted. run: | KEY=$(openssl rand -hex 32) sed -i "s/^throttle\.key=''/throttle.key='$KEY'/" .env grep -Eq "^throttle\.key=.+" .env - name: Run PHPUnit tests env: CI_ENVIRONMENT: testing MYSQL_HOST_NAME: 127.0.0.1 run: composer test -- --no-coverage --log-junit test-results/junit.xml - name: Set artifact name if: always() run: echo "DB_LABEL=$(echo '${{ matrix.db-image }}' | tr ':' '-')" >> $GITHUB_ENV - name: Upload test results uses: actions/upload-artifact@v4 if: always() with: name: test-results-php-${{ matrix.php-version }}-${{ env.DB_LABEL }} path: test-results/ retention-days: 30 - name: Stop database if: always() run: docker stop mysql && docker rm mysql