diff --git a/.config/.remarkrc b/.config/.remarkrc new file mode 100644 index 0000000..bfa065d --- /dev/null +++ b/.config/.remarkrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + "remark-preset-lint-recommended", + ["remark-lint-list-item-indent", "space"] + ] +} diff --git a/.config/.yamllint b/.config/.yamllint new file mode 100644 index 0000000..7c1b6e4 --- /dev/null +++ b/.config/.yamllint @@ -0,0 +1,14 @@ +--- +extends: default + +ignore: | + vendor/ + +rules: + brackets: + max-spaces-inside: 1 + document-start: disable + line-length: + level: warning + max: 120 + truthy: {allowed-values: ["true", "false", "on"]} diff --git a/.github/workflows/json.yml b/.github/workflows/json.yml new file mode 100644 index 0000000..7e83269 --- /dev/null +++ b/.github/workflows/json.yml @@ -0,0 +1,46 @@ +--- +name: JSON Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.json' + - '.github/workflows/json.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.json' + - '.github/workflows/json.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.json.lint-syntax.yml + lint-json-syntax: + name: JSON Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/jsonlint + with: + args: >- + find . + -not -path '*/.git/*' + -not -path '*/node_modules/*' + -not -path '*/vendor/*' + -name '*.json' + -type f + -exec jsonlint --quiet {} ; diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml new file mode 100644 index 0000000..581b9c7 --- /dev/null +++ b/.github/workflows/markdown.yml @@ -0,0 +1,42 @@ +--- +name: Markdown Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.md' + - '.github/workflows/markdown.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.md' + - '.github/workflows/markdown.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.quality.markdown.lint-syntax.yml + lint-markdown-syntax: + name: Markdown Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/remark-lint + with: + args: >- + remark + --rc-path=.config/.remarkrc + --ignore-pattern='*/vendor/*' diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..85c820c --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,134 @@ +--- +name: PHP Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + paths: + - '**.php' + - '.config/phpcs.xml.dist' + - 'tests/phpunit/phpunit.xml' + - '.github/workflows/php.yml' + - 'composer.json' + - 'composer.lock' + branches: [ main, feature/php-ci ] + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.php' + - '.config/phpcs.xml.dist' + - 'tests/phpunit/phpunit.xml' + - '.github/workflows/php.yml' + - 'composer.json' + - 'composer.lock' + # Allow manually triggering the workflow. + workflow_dispatch: + + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.php.lint-syntax.yml + lint-php-syntax: + name: PHP Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-linter + with: + args: >- + parallel-lint + --exclude .git + --exclude vendor + --no-progress + . +# # 01.quality.php.validate.dependencies-file.yml + validate-dependencies-file: + name: Validate dependencies file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + composer validate + --check-lock + --no-plugins + --no-scripts + --strict + # 02.test.php.test-unit.yml + php-unittest: + name: PHP Unit Tests + needs: + - lint-php-syntax + - validate-dependencies-file + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: + - '8.1' # from 2021-11 to 2023-11 (2025-12) + - '8.2' # from 2022-12 to 2024-12 (2026-12) + - '8.3' # from 2023-11 to 2025-12 (2027-12) + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + coverage: xdebug + ini-values: error_reporting=E_ALL, display_errors=On + php-version: ${{ matrix.php }} + - name: Install and Cache Composer dependencies + uses: "ramsey/composer-install@v2" + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' + - run: vendor/bin/phpunit --configuration tests/phpunit/phpunit.xml + # 03.quality.php.scan.dependencies-vulnerabilities.yml + scan-dependencies-vulnerabilities: + name: Scan Dependencies Vulnerabilities + needs: + - validate-dependencies-file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install and Cache Composer dependencies + uses: "ramsey/composer-install@v2" + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' + - run: >- + composer audit + --abandoned=report + --no-dev + --no-plugins + --no-scripts + # 03.quality.php.lint-version-compatibility.yml + php-check-version-compatibility: + name: PHP Version Compatibility + needs: + - lint-php-syntax + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: + - '8.1' # from 2021-11 to 2023-11 (2025-12) + - '8.2' # from 2022-12 to 2024-12 (2026-12) + - '8.3' # from 2023-11 to 2025-12 (2027-12) + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-codesniffer + with: + args: >- + phpcs + -s + --extensions=php + --ignore='*vendor/*' + --runtime-set testVersion ${{ matrix.php }} + --standard=PHPCompatibility + . diff --git a/.github/workflows/yaml.yml b/.github/workflows/yaml.yml new file mode 100644 index 0000000..ad8fb9d --- /dev/null +++ b/.github/workflows/yaml.yml @@ -0,0 +1,42 @@ +--- +name: YAML Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.yml' + - '**.yaml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.yml' + - '**.yaml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.yaml.lint.yml + lint-yaml: + name: YAML Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/yamllint + with: + args: >- + yamllint + --config-file=.config/.yamllint + . diff --git a/TODO b/TODO index fb86193..0e4659a 100644 --- a/TODO +++ b/TODO @@ -57,4 +57,21 @@ - [v] webid - [v] wac - [v] solid-crud -- [v] CI integration \ No newline at end of file +- [v] CI integration + +------ Unit tests ----- +- [v] ClientRegistration +- [v] JtiStore +- [v] IpAttempts +- [v] Util +- [v] PasswordValidator +- [ ] Mailer +- [ ] MailTemplateGenerator +- [ ] MailTemplates +- [ ] Server +- [ ] SolidNotifications +- [ ] SolidPubSub +- [ ] StorageServer +- [ ] User +- [-] Middleware +- [-] Db diff --git a/composer.json b/composer.json index d0e1836..1b4d9e0 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "pdsinterop/php-solid", + "description": "Multi-user Solid Server for PHP", "type": "project", "license": "MIT", "autoload": { @@ -17,12 +18,12 @@ "pdsinterop/solid-auth": "v0.13.0", "pdsinterop/solid-crud": "v0.8.1", "phpmailer/phpmailer": "^6.10", - "sweetrdf/easyrdf": "v1.15", + "sweetrdf/easyrdf": "~1.15.0", "phpseclib/bcmath_compat": "^2.0", "phrity/websocket": "^3.5" }, "require-dev": { - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^9 || ^10 || ^11 || ^12", "phpstan/phpstan": "^2.1" } } diff --git a/init.php b/init.php index 8bdba37..52d3d89 100644 --- a/init.php +++ b/init.php @@ -32,7 +32,7 @@ function initDatabase() { )', 'CREATE TABLE IF NOT EXISTS jti ( jti VARCHAR(255) NOT NULL PRIMARY KEY, - expires TEXT + expires TEXT NOT NULL )', 'CREATE TABLE IF NOT EXISTS users ( user_id VARCHAR(255) NOT NULL PRIMARY KEY, @@ -43,7 +43,7 @@ function initDatabase() { 'CREATE TABLE IF NOT EXISTS ipAttempts ( ip VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, - expires NOT NULL + expires TEXT NOT NULL )', ]; diff --git a/lib/ClientRegistration.php b/lib/ClientRegistration.php index a0c354c..88d98a0 100644 --- a/lib/ClientRegistration.php +++ b/lib/ClientRegistration.php @@ -1,17 +1,12 @@ prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT clientData FROM clients WHERE clientId=:clientId' ); $query->execute([ @@ -25,11 +20,11 @@ public static function getRegistration($clientId) { } public static function saveClientRegistration($clientData) { - self::connect(); + Db::connect(); if (!isset($clientData['client_name'])) { $clientData['client_name'] = $clientData['origin']; } - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'INSERT INTO clients VALUES(:clientId, :origin, :clientData)' ); $query->execute([ @@ -40,8 +35,8 @@ public static function saveClientRegistration($clientData) { } public static function getClientByOrigin($origin) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT clientData FROM clients WHERE origin=:origin' ); $query->execute([ diff --git a/lib/Db.php b/lib/Db.php new file mode 100644 index 0000000..8eb46e1 --- /dev/null +++ b/lib/Db.php @@ -0,0 +1,11 @@ +prepare( + $query = Db::$pdo->prepare( 'INSERT INTO ipAttempts VALUES(:ip, :type, :expires)' ); $query->execute([ @@ -31,10 +26,10 @@ public static function getAttemptsCount($ip, $type) { return 0; } - self::connect(); + Db::connect(); $now = new \DateTime(); - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'SELECT count(ip) as count FROM ipAttempts WHERE ip=:ip AND type=:type AND expires > :now' ); $query->execute([ @@ -49,10 +44,10 @@ public static function getAttemptsCount($ip, $type) { return 0; } public static function cleanupAttempts() { - self::connect(); + Db::connect(); $now = new \DateTime(); - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'DELETE FROM ipAttempts WHERE expires < :now' ); $query->execute([ diff --git a/lib/JtiStore.php b/lib/JtiStore.php index bbf7861..b487059 100644 --- a/lib/JtiStore.php +++ b/lib/JtiStore.php @@ -1,19 +1,12 @@ prepare( + $query = Db::$pdo->prepare( 'SELECT jti FROM jti WHERE jti=:jti AND expires>:now' ); $query->execute([ @@ -28,8 +21,8 @@ public static function hasJti($jti) { } public static function saveJti($jti) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT INTO jti VALUES(:jti, :expires)' ); $expires = new \DateTime(); @@ -39,4 +32,15 @@ public static function saveJti($jti) { ':expires' => $expires->getTimestamp() ]); } + + public static function cleanupJti() { + Db::connect(); + $now = new \DateTime(); + $query = Db::$pdo->prepare( + 'DELETE FROM jti WHERE expires < :now' + ); + $query->execute([ + ':now' => $now->getTimestamp() + ]); + } } \ No newline at end of file diff --git a/lib/User.php b/lib/User.php index 66c5505..22a2527 100644 --- a/lib/User.php +++ b/lib/User.php @@ -2,15 +2,9 @@ namespace Pdsinterop\PhpSolid; use Pdsinterop\PhpSolid\PasswordValidator; - + use Pdsinterop\PhpSolid\Db; + class User { - private static $pdo; - private static function connect() { - if (!isset(self::$pdo)) { - self::$pdo = new \PDO("sqlite:" . DBPATH); - } - } - private static function generateTokenCode() { $digits = 6; $code = random_int(0,1000000); @@ -42,8 +36,8 @@ public static function saveVerifyToken($tokenType, $tokenData) { break; } - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT INTO verify VALUES(:code, :data)' ); $query->execute([ @@ -54,8 +48,8 @@ public static function saveVerifyToken($tokenType, $tokenData) { } public static function getVerifyToken($code) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT data FROM verify WHERE code=:code' ); $query->execute([ @@ -84,13 +78,14 @@ public static function validatePasswordStrength($password) { $entropy = PasswordValidator::getEntropy($password, BANNED_PASSWORDS); $minimumEntropy = MINIMUM_PASSWORD_ENTROPY; if ($entropy < $minimumEntropy) { + error_log("Entered pasword does not satisfy minimum entropy"); return false; } return true; } public static function createUser($newUser) { - self::connect(); + Db::connect(); if (!self::validatePasswordStrength($newUser['password'])) { return false; } @@ -98,7 +93,7 @@ public static function createUser($newUser) { while (self::userIdExists($generatedUserId)) { $generatedUserId = md5(random_bytes(32)); } - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'INSERT INTO users VALUES (:userId, :email, :passwordHash, :data)' ); @@ -126,8 +121,8 @@ public static function setUserPassword($email, $newPassword) { if (!self::validatePasswordStrength($newPassword)) { return false; } - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'UPDATE users SET password=:passwordHash WHERE email=:email' ); $queryParams = []; @@ -139,8 +134,8 @@ public static function setUserPassword($email, $newPassword) { } public static function allowClientForUser($clientId, $userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT OR REPLACE INTO allowedClients VALUES(:userId, :clientId)' ); $query->execute([ @@ -151,8 +146,8 @@ public static function allowClientForUser($clientId, $userId) { } public static function getAllowedClients($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT clientId FROM allowedClients WHERE userId=:userId' ); $query->execute([ @@ -166,8 +161,8 @@ public static function getAllowedClients($userId) { } public static function getStorage($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT storageUrl FROM userStorage WHERE userId=:userId' ); $query->execute([ @@ -181,8 +176,8 @@ public static function getStorage($userId) { } public static function setStorage($userId, $storageUrl) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT OR REPLACE INTO storage VALUES(:userId, :storageUrl)' ); $query->execute([ @@ -192,8 +187,8 @@ public static function setStorage($userId, $storageUrl) { } public static function getUser($email) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id, data FROM users WHERE email=:email' ); $query->execute([ @@ -217,8 +212,8 @@ public static function getUser($email) { } public static function getUserById($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id, data FROM users WHERE user_id=:userId' ); $query->execute([ @@ -242,8 +237,8 @@ public static function getUserById($userId) { } public static function checkPassword($email, $password) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT password FROM users WHERE email=:email' ); $query->execute([ @@ -271,8 +266,8 @@ public static function getLoggedInUser() { } public static function userIdExists($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id FROM users WHERE user_id=:userId' ); $query->execute([ @@ -286,8 +281,8 @@ public static function userIdExists($userId) { } public static function userEmailExists($email) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id FROM users WHERE email=:email' ); $query->execute([ @@ -301,8 +296,8 @@ public static function userEmailExists($email) { } private static function deleteUser($email) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'DELETE FROM users WHERE email=:email' ); $query->execute([ @@ -316,8 +311,8 @@ private static function deleteAllowedClients($email) { return; } - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'DELETE FROM allowedClients WHERE userId=:userId' ); $query->execute([ @@ -335,10 +330,10 @@ public static function deleteAccount($email) { } public static function cleanupTokens() { - self::connect(); + Db::connect(); $now = new \DateTime(); - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'DELETE FROM verify WHERE json_extract(data, \'$.expires\') < :now' ); $query->execute([ diff --git a/tests/phpunit/ClientRegistrationTest.php b/tests/phpunit/ClientRegistrationTest.php new file mode 100644 index 0000000..8616fde --- /dev/null +++ b/tests/phpunit/ClientRegistrationTest.php @@ -0,0 +1,74 @@ +exec($statement); + } + } catch(\PDOException $e) { + echo $e->getMessage(); + } + + ClientRegistration::saveClientRegistration([ + "client_id" => "12345", + "origin" => "https://example.com", + "client_name" => "Client name" + ]); + + ClientRegistration::saveClientRegistration([ + "client_id" => "23456", + "origin" => "https://example2.com" + ]); + + ClientRegistration::saveClientRegistration([ + "client_id" => "34567", + "origin" => "https://example2.com" + ]); + } + + public function testGetRegistration() { + $storedData = ClientRegistration::getRegistration("12345"); + $this->assertEquals("12345", $storedData['client_id']); + $this->assertEquals("https://example.com", $storedData['origin']); + $this->assertEquals("Client name", $storedData['client_name']); + } + + public function testClientNameAutofill() { + $storedData = ClientRegistration::getRegistration("23456"); + $this->assertEquals("23456", $storedData['client_id']); + $this->assertEquals("https://example2.com", $storedData['origin']); + $this->assertEquals("https://example2.com", $storedData['client_name']); + } + + public function testClientByOrigin() { + $storedData = ClientRegistration::getClientByOrigin("https://example.com"); + $this->assertEquals("12345", $storedData['client_id']); + $this->assertEquals("https://example.com", $storedData['origin']); + $this->assertEquals("Client name", $storedData['client_name']); + } + + public function testClientByDuplicateOrigin() { + $storedData = ClientRegistration::getClientByOrigin("https://example2.com"); + $this->assertFalse($storedData); // false because we have 2 clients with the same origin + } +} diff --git a/tests/phpunit/IpAttemptsTest.php b/tests/phpunit/IpAttemptsTest.php new file mode 100644 index 0000000..df0373d --- /dev/null +++ b/tests/phpunit/IpAttemptsTest.php @@ -0,0 +1,123 @@ +exec($statement); + } + } catch(\PDOException $e) { + echo $e->getMessage(); + } + } + + public function testGetAttemptsCountZero() { + $ip = "10.0.0.1"; + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(0, $count); + } + + public function testGetAttemptsCountOne() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(1, $count); + } + + public function testGetAttemptsCountTwo() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(2, $count); + } + + public function testGetAttemptsCountExpired() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(0, $count); + } + + public function testGetAttemptsCountOneExpired() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() + 10); + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(1, $count); + } + + public function testCleanup() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM ipAttempts'); + $query->execute(); + $result = $query->fetchAll(); + $beforeCleanup = $result[0]['count']; + + $this->assertEquals(4, $beforeCleanup); + + IpAttempts::cleanupAttempts(); + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM ipAttempts'); + $query->execute(); + $result = $query->fetchAll(); + $afterCleanup = $result[0]['count']; + + $this->assertEquals(2, $afterCleanup); + } + + public function testTrustedIpGetAttempts() { + $ip = "127.0.0.100"; // trusted IP + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(0, $count); + } + + public function testTrustedIpGetAttemptsSkipsDb() { + $ip = "127.0.0.100"; // trusted IP + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM ipAttempts'); + $query->execute(); + $result = $query->fetchAll(); + $count = $result[0]['count']; + $this->assertEquals(0, $count); + } +} diff --git a/tests/phpunit/JtiStoreTest.php b/tests/phpunit/JtiStoreTest.php new file mode 100644 index 0000000..602afa4 --- /dev/null +++ b/tests/phpunit/JtiStoreTest.php @@ -0,0 +1,87 @@ +exec($statement); + } + } catch(\PDOException $e) { + echo $e->getMessage(); + } + } + + public function testNonExistingJti() { + $jti = "123"; + $found = JtiStore::hasJti($jti); + $this->assertFalse($found); + } + + public function testExistingJti() { + $jti = "123"; + JtiStore::saveJti($jti, time() + 3600); + $found = JtiStore::hasJti($jti); + $this->assertTrue($found); + } + + public function testExpiredJti() { + $jti = "123"; + JtiStore::saveJti($jti); + $query = Db::$pdo->prepare('UPDATE jti SET expires=:expires WHERE jti=:jti'); + $query->execute([ + 'expires' => time() - 10, + 'jti' => $jti + ]); + $found = JtiStore::hasJti($jti); + $this->assertFalse($found); + } + + public function testCleanup() { + JtiStore::saveJti("123"); + JtiStore::saveJti("234"); + + $query = Db::$pdo->prepare('UPDATE jti SET expires=:expires WHERE jti=:jti'); + $query->execute([ + 'expires' => time() - 10, + 'jti' => "123" + ]); + $query = Db::$pdo->prepare('UPDATE jti SET expires=:expires WHERE jti=:jti'); + $query->execute([ + 'expires' => time() - 10, + 'jti' => "234" + ]); + + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM jti'); + $query->execute(); + $result = $query->fetchAll(); + $beforeCleanup = $result[0]['count']; + $this->assertEquals(2, $beforeCleanup); + + + JtiStore::cleanupJti(); + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM jti'); + $query->execute(); + $result = $query->fetchAll(); + $afterCleanup = $result[0]['count']; + + $this->assertEquals(0, $afterCleanup); + } +} diff --git a/tests/phpunit/PasswordValidatorTest.php b/tests/phpunit/PasswordValidatorTest.php new file mode 100644 index 0000000..9930219 --- /dev/null +++ b/tests/phpunit/PasswordValidatorTest.php @@ -0,0 +1,79 @@ +assertEquals(26, PasswordValidator::getBase($password)); + } + + public function testBaseUpper() + { + $password = "AAA"; + $this->assertEquals(26, PasswordValidator::getBase($password)); + } + + public function testBaseNumbers() + { + $password = "123"; + $this->assertEquals(10, PasswordValidator::getBase($password)); + } + + public function testBaseSpecial() + { + $password = "!@#$"; + $this->assertEquals(32, PasswordValidator::getBase($password)); + } + + public function testBaseUpperAndLower() + { + $password = "aaaAAA"; + $this->assertEquals(52, PasswordValidator::getBase($password)); + } + + public function testLengthLower() + { + $password = "aaa"; + $this->assertEquals(2, PasswordValidator::getLength($password)); + } + + public function testLengthUpper() + { + $password = "AAA"; + $this->assertEquals(2, PasswordValidator::getLength($password)); + } + + public function testLengthNumbers() + { + $password = "123"; + $this->assertEquals(3, PasswordValidator::getLength($password)); + } + + public function testLengthSpecial() + { + $password = "!@#$"; + $this->assertEquals(4, PasswordValidator::getLength($password)); + } + + public function testLengthUpperAndLower() + { + $password = "aaaAAA"; + $this->assertEquals(4, PasswordValidator::getLength($password)); + } + + public function testSimplyEntropy() + { + $values = [ + ["password" => "aaa", "expected" => 6.52], + ["password" => "abc", "expected" => 9.77] + ]; + + foreach ($values as $value) { + $this->assertEquals($value['expected'], PasswordValidator::getEntropy($value['password'])); + } + } +} diff --git a/tests/phpunit/UtilTest.php b/tests/phpunit/UtilTest.php new file mode 100644 index 0000000..c92b71b --- /dev/null +++ b/tests/phpunit/UtilTest.php @@ -0,0 +1,14 @@ +assertEquals($string, $decoded); + } +} diff --git a/tests/phpunit/phpunit.xml b/tests/phpunit/phpunit.xml new file mode 100644 index 0000000..e883c5d --- /dev/null +++ b/tests/phpunit/phpunit.xml @@ -0,0 +1,7 @@ + + + + . + + + diff --git a/tests/phpunit/test-config.php b/tests/phpunit/test-config.php new file mode 100644 index 0000000..c917c6c --- /dev/null +++ b/tests/phpunit/test-config.php @@ -0,0 +1,5 @@ +