From bcaee273fab94cac2d19e9dd5e517db60a04e79a Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Sun, 14 Jun 2026 03:06:34 +0200 Subject: [PATCH 1/2] :white_check_mark: test: CredentialEncryption-Unit-Tests + CLI-Compose-Runner + GitHub-Action [*] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit-Tests (PHPUnit 12) für CredentialEncryption, CLI-only docker-compose-Runner (composer install + vendor/bin/phpunit), GitHub-Actions-Workflow (PHP 8.2/8.4, ext-sodium), .gitignore. phpunit ^12 in require-dev. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/tests.yml | 25 ++++++ .gitignore | 4 + composer.json | 3 + docker-compose.yml | 17 ++++ phpunit.dist.xml | 18 ++++ tests/Service/CredentialEncryptionTest.php | 96 ++++++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 docker-compose.yml create mode 100644 phpunit.dist.xml create mode 100644 tests/Service/CredentialEncryptionTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..99c7e0c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,25 @@ +# file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC +name: Tests + +on: + push: + branches: [main, master, "feature/**"] + pull_request: + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ["8.2", "8.4"] + name: PHPUnit (PHP ${{ matrix.php }}) + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: sodium, intl + coverage: none + - run: composer install --no-interaction --no-progress + - run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cff2af8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC +/vendor/ +/composer.lock +/.phpunit.cache/ diff --git a/composer.json b/composer.json index a6c14fc..aee60d9 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,9 @@ "doctrine/orm": "^2.0|^3.0", "psr/log": "^3.0" }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, "autoload": { "psr-4": { "Dmstr\\ApiPlatformUtils\\": "src/" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ac05e13 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +# file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC +# CLI-only test runner for this library — no MySQL/FPM required. +# Usage (on host): +# docker compose run --rm php +# Runs `composer install` then PHPUnit against tests/. +services: + php: + build: + dockerfile_inline: | + FROM php:8.4-cli-alpine + COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + RUN install-php-extensions sodium intl + COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + working_dir: /repo + volumes: + - .:/repo + command: sh -c "composer install --no-interaction --no-progress && vendor/bin/phpunit" diff --git a/phpunit.dist.xml b/phpunit.dist.xml new file mode 100644 index 0000000..9dc4142 --- /dev/null +++ b/phpunit.dist.xml @@ -0,0 +1,18 @@ + + + + + + tests + + + + + src + + + diff --git a/tests/Service/CredentialEncryptionTest.php b/tests/Service/CredentialEncryptionTest.php new file mode 100644 index 0000000..0295752 --- /dev/null +++ b/tests/Service/CredentialEncryptionTest.php @@ -0,0 +1,96 @@ + 'alice', 'password' => 's3cr3t!', 'scopes' => ['read', 'write']]; + + $cipher = $service->encrypt($credentials); + self::assertNotSame('', $cipher); + self::assertSame($credentials, $service->decrypt($cipher)); + } + + public function testGenerateKeyProducesUsable32ByteKey(): void + { + $encoded = CredentialEncryption::generateKey(); + $raw = base64_decode($encoded, true); + + self::assertNotFalse($raw, 'generateKey() must return valid base64'); + self::assertSame(SODIUM_CRYPTO_SECRETBOX_KEYBYTES, \strlen($raw)); + + // A freshly generated key must work for a real round-trip. + $service = new CredentialEncryption($raw); + self::assertSame(['a' => 1], $service->decrypt($service->encrypt(['a' => 1]))); + } + + public function testConstructorRejectsWrongKeyLength(): void + { + $this->expectException(\InvalidArgumentException::class); + new CredentialEncryption('too-short'); + } + + public function testDecryptRejectsInvalidBase64(): void + { + $service = new CredentialEncryption(self::KEY); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid base64 encoding'); + $service->decrypt('###-not-base64-###'); + } + + public function testDecryptRejectsTamperedCiphertext(): void + { + $service = new CredentialEncryption(self::KEY); + $cipher = $service->encrypt(['token' => 'abc']); + + // Flip the final byte of the authenticated ciphertext. + $raw = base64_decode($cipher, true); + $raw[\strlen($raw) - 1] = $raw[\strlen($raw) - 1] === "\x00" ? "\x01" : "\x00"; + $tampered = base64_encode($raw); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Decryption failed'); + $service->decrypt($tampered); + } + + public function testDecryptWithWrongKeyFails(): void + { + $cipher = (new CredentialEncryption(self::KEY))->encrypt(['token' => 'abc']); + + $otherKey = 'fedcba9876543210fedcba9876543210'; + $this->expectException(\RuntimeException::class); + (new CredentialEncryption($otherKey))->decrypt($cipher); + } + + public function testEncryptIsNonDeterministicPerNonce(): void + { + $service = new CredentialEncryption(self::KEY); + $payload = ['username' => 'bob']; + + self::assertNotSame( + $service->encrypt($payload), + $service->encrypt($payload), + 'Each encryption must use a fresh random nonce.' + ); + } +} From 4f40fb23de5c0b0fa17e579a006bbf976d5b3f5e Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Sun, 14 Jun 2026 03:28:24 +0200 Subject: [PATCH 2/2] =?UTF-8?q?:green=5Fheart:=20ci:=20PHP-Matrix=20auf=20?= =?UTF-8?q?8.3/8.4=20(phpunit=20^12=20ben=C3=B6tigt=20PHP=20>=3D8.3)=20[*]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8.2 fiel raus: PHPUnit 12 verlangt PHP >=8.3, daher schlug composer install im 8.2-Job fehl. Runtime-Constraint des Pakets (>=8.2) bleibt unverändert. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 99c7e0c..23e6bf0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - php: ["8.2", "8.4"] + php: ["8.3", "8.4"] name: PHPUnit (PHP ${{ matrix.php }}) steps: - uses: actions/checkout@v4