diff --git a/README.md b/README.md index 13650de1119ed248fa68b3a98607e4dc99ed2dc8..02d784ad7a734ad17dc8638a8859a243eb425971 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,17 @@ # simplesamlphp-module-accounting -SimpleSAMLphp module providing user accounting functionality +SimpleSAMLphp module providing user accounting functionality. + +**Module is in development and is not production ready.** ## TODO -- [ ] MySQL store -- [ ] MySQL Job Store -- [ ] Cron hooks \ No newline at end of file +- [x] Data stores +- [ ] Cron hooks +- [ ] Profile page UI +- [ ] Translation + +## Tests +To run phpcs, psalm and phpunit: + +```shell +composer pre-commit +``` diff --git a/composer.json b/composer.json index d88308a618cf7b54713f84fa21da33ef86d4d2e5..aa085a84415da5fe260fc9f4bb6ef18f4b22b8dc 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,6 @@ "require": { "php": "^7.4 || ^8.0", "ext-pdo": "*", - "ext-pdo_mysql": "*", "ext-pdo_sqlite": "*", "doctrine/dbal": "^3", "psr/log": "^1|^2|^3", diff --git a/composer.lock b/composer.lock index 464fd3d9ead5c9eb45c4d675936d88954e6960b4..1ae8db2b89c6a786d3f691f9491dfcc2963f47d3 100644 --- a/composer.lock +++ b/composer.lock @@ -157,16 +157,16 @@ }, { "name": "composer/composer", - "version": "2.4.0", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "026d6de6ea2c913974a7756661a3faac135cb36e" + "reference": "7d887621e69a0311eb50aed4a16f7044b2b385b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/026d6de6ea2c913974a7756661a3faac135cb36e", - "reference": "026d6de6ea2c913974a7756661a3faac135cb36e", + "url": "https://api.github.com/repos/composer/composer/zipball/7d887621e69a0311eb50aed4a16f7044b2b385b9", + "reference": "7d887621e69a0311eb50aed4a16f7044b2b385b9", "shasum": "" }, "require": { @@ -196,7 +196,7 @@ "phpstan/phpstan-deprecation-rules": "^1", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1", - "phpstan/phpstan-symfony": "^1.1", + "phpstan/phpstan-symfony": "^1.2.10", "symfony/phpunit-bridge": "^6.0" }, "suggest": { @@ -249,7 +249,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.4.0" + "source": "https://github.com/composer/composer/tree/2.4.2" }, "funding": [ { @@ -265,7 +265,7 @@ "type": "tidelift" } ], - "time": "2022-08-16T14:10:48+00:00" + "time": "2022-09-14T14:11:15+00:00" }, { "name": "composer/metadata-minifier", @@ -729,16 +729,16 @@ }, { "name": "doctrine/dbal", - "version": "3.4.1", + "version": "3.4.5", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "94e016428884227245fb1219e0de7d8b86ca16d7" + "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/94e016428884227245fb1219e0de7d8b86ca16d7", - "reference": "94e016428884227245fb1219e0de7d8b86ca16d7", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/a5a58773109c0abb13e658c8ccd92aeec8d07f9e", + "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e", "shasum": "" }, "require": { @@ -751,16 +751,16 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "9.0.0", - "jetbrains/phpstorm-stubs": "2022.1", - "phpstan/phpstan": "1.8.2", + "doctrine/coding-standard": "10.0.0", + "jetbrains/phpstorm-stubs": "2022.2", + "phpstan/phpstan": "1.8.3", "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "9.5.21", + "phpunit/phpunit": "9.5.24", "psalm/plugin-phpunit": "0.17.0", "squizlabs/php_codesniffer": "3.7.1", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", - "vimeo/psalm": "4.24.0" + "vimeo/psalm": "4.27.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -820,7 +820,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.4.1" + "source": "https://github.com/doctrine/dbal/tree/3.4.5" }, "funding": [ { @@ -836,7 +836,7 @@ "type": "tidelift" } ], - "time": "2022-08-16T18:37:46+00:00" + "time": "2022-09-23T17:48:57+00:00" }, { "name": "doctrine/deprecations", @@ -1266,16 +1266,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.6.3", + "version": "v6.6.4", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "9400f305a898f194caff5521f64e5dfa926626f3" + "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/9400f305a898f194caff5521f64e5dfa926626f3", - "reference": "9400f305a898f194caff5521f64e5dfa926626f3", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/a94fdebaea6bd17f51be0c2373ab80d3d681269b", + "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b", "shasum": "" }, "require": { @@ -1332,7 +1332,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.3" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.4" }, "funding": [ { @@ -1340,7 +1340,7 @@ "type": "github" } ], - "time": "2022-06-20T09:21:02+00:00" + "time": "2022-08-22T09:22:00+00:00" }, { "name": "psr/cache", @@ -1723,16 +1723,16 @@ }, { "name": "seld/phar-utils", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "9f3452c93ff423469c0d56450431562ca423dcee" + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/9f3452c93ff423469c0d56450431562ca423dcee", - "reference": "9f3452c93ff423469c0d56450431562ca423dcee", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", "shasum": "" }, "require": { @@ -1765,9 +1765,9 @@ ], "support": { "issues": "https://github.com/Seldaek/phar-utils/issues", - "source": "https://github.com/Seldaek/phar-utils/tree/1.2.0" + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" }, - "time": "2021-12-10T11:20:11+00:00" + "time": "2022-08-31T10:31:18+00:00" }, { "name": "seld/signal-handler", @@ -1832,16 +1832,16 @@ }, { "name": "simplesamlphp/assert", - "version": "v0.3.3", + "version": "v0.8.0", "source": { "type": "git", "url": "https://github.com/simplesamlphp/assert.git", - "reference": "9af4fe36e7b2a9c4d0695a8e8ad118c39b9e3564" + "reference": "d3b0f38f4ae083822471c15e3c4a0401ddaeac73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/9af4fe36e7b2a9c4d0695a8e8ad118c39b9e3564", - "reference": "9af4fe36e7b2a9c4d0695a8e8ad118c39b9e3564", + "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/d3b0f38f4ae083822471c15e3c4a0401ddaeac73", + "reference": "d3b0f38f4ae083822471c15e3c4a0401ddaeac73", "shasum": "" }, "require": { @@ -1855,7 +1855,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "v0.2.x-dev" + "dev-master": "v0.8.x-dev" } }, "autoload": { @@ -1880,35 +1880,36 @@ "description": "A wrapper around webmozart/assert to make it useful beyond checking method arguments", "support": { "issues": "https://github.com/simplesamlphp/assert/issues", - "source": "https://github.com/simplesamlphp/assert/tree/v0.3.3" + "source": "https://github.com/simplesamlphp/assert/tree/v0.8.0" }, - "time": "2022-08-02T21:14:39+00:00" + "time": "2022-09-20T20:18:55+00:00" }, { "name": "simplesamlphp/composer-module-installer", - "version": "v1.1.8", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/simplesamlphp/composer-module-installer.git", - "reference": "45161b5406f3e9c82459d0f9a5a1dba064953cfa" + "reference": "27b4fe96198ffaff3ab49c87b40f4cb24de77b01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/45161b5406f3e9c82459d0f9a5a1dba064953cfa", - "reference": "45161b5406f3e9c82459d0f9a5a1dba064953cfa", + "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/27b4fe96198ffaff3ab49c87b40f4cb24de77b01", + "reference": "27b4fe96198ffaff3ab49c87b40f4cb24de77b01", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1|^2.0", + "composer-plugin-api": "^1.1 || ^2.0", + "php": "^7.4 || ^8.0", "simplesamlphp/simplesamlphp": "*" }, "type": "composer-plugin", "extra": { - "class": "SimpleSamlPhp\\Composer\\ModuleInstallerPlugin" + "class": "SimpleSAML\\Composer\\ModuleInstallerPlugin" }, "autoload": { - "psr-0": { - "SimpleSamlPhp\\Composer": "src/" + "psr-4": { + "SimpleSAML\\Composer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1918,9 +1919,9 @@ "description": "A Composer plugin that allows installing SimpleSAMLphp modules through Composer.", "support": { "issues": "https://github.com/simplesamlphp/composer-module-installer/issues", - "source": "https://github.com/simplesamlphp/composer-module-installer/tree/v1.1.8" + "source": "https://github.com/simplesamlphp/composer-module-installer/tree/v1.2.0" }, - "time": "2020-08-25T19:04:33+00:00" + "time": "2022-08-31T17:20:27+00:00" }, { "name": "simplesamlphp/saml2", @@ -1982,16 +1983,16 @@ }, { "name": "simplesamlphp/simplesamlphp", - "version": "v2.0.0-rc1", + "version": "v2.0.0-rc2", "source": { "type": "git", "url": "https://github.com/simplesamlphp/simplesamlphp.git", - "reference": "cf68e6ef7aaecf75d434023adfe30913d993f015" + "reference": "2cf4ec863ab9aa59eb4ad0b5287ab8ab97360089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/cf68e6ef7aaecf75d434023adfe30913d993f015", - "reference": "cf68e6ef7aaecf75d434023adfe30913d993f015", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/2cf4ec863ab9aa59eb4ad0b5287ab8ab97360089", + "reference": "2cf4ec863ab9aa59eb4ad0b5287ab8ab97360089", "shasum": "" }, "require": { @@ -2010,7 +2011,7 @@ "gettext/translator": "^1.0.1", "php": ">=7.4 || ^8.0", "phpmailer/phpmailer": "^6.5", - "simplesamlphp/assert": "^0.3.0", + "simplesamlphp/assert": "^0.8.0", "simplesamlphp/saml2": "^4.6", "symfony/cache": "^5.4", "symfony/config": "^5.4", @@ -2034,9 +2035,9 @@ "ext-curl": "*", "ext-pdo_sqlite": "*", "mikey179/vfsstream": "~1.6", - "simplesamlphp/simplesamlphp-module-adfs": ">=2.0.0-rc2", + "simplesamlphp/simplesamlphp-module-adfs": ">=2.0.0-rc5", "simplesamlphp/simplesamlphp-test-framework": "^1.2.1", - "simplesamlphp/xml-security": "^0.4.5" + "simplesamlphp/xml-security": "^0.6.6" }, "suggest": { "ext-curl": "Needed in order to check for updates automatically", @@ -2094,7 +2095,7 @@ "issues": "https://github.com/simplesamlphp/simplesamlphp/issues", "source": "https://github.com/simplesamlphp/simplesamlphp" }, - "time": "2022-07-01T19:47:50+00:00" + "time": "2022-09-22T06:38:05+00:00" }, { "name": "symfony/cache", @@ -2353,16 +2354,16 @@ }, { "name": "symfony/console", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", + "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", "shasum": "" }, "require": { @@ -2432,7 +2433,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.11" + "source": "https://github.com/symfony/console/tree/v5.4.12" }, "funding": [ { @@ -2448,7 +2449,7 @@ "type": "tidelift" } ], - "time": "2022-07-22T10:42:43+00:00" + "time": "2022-08-17T13:18:05+00:00" }, { "name": "symfony/dependency-injection", @@ -2843,16 +2844,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd" + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6699fb0228d1bc35b12aed6dd5e7455457609ddd", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d67c1f9a1937406a9be3171b4b22250c0a11447", + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447", "shasum": "" }, "require": { @@ -2887,7 +2888,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.11" + "source": "https://github.com/symfony/filesystem/tree/v5.4.12" }, "funding": [ { @@ -2903,7 +2904,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-02T13:48:16+00:00" }, { "name": "symfony/finder", @@ -2970,16 +2971,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "a0660b602357d5c2ceaac1c9f80c5820bbff803d" + "reference": "49f8fe5d39b7513a3f26898788885dbe66b0d910" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a0660b602357d5c2ceaac1c9f80c5820bbff803d", - "reference": "a0660b602357d5c2ceaac1c9f80c5820bbff803d", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/49f8fe5d39b7513a3f26898788885dbe66b0d910", + "reference": "49f8fe5d39b7513a3f26898788885dbe66b0d910", "shasum": "" }, "require": { @@ -3101,7 +3102,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v5.4.11" + "source": "https://github.com/symfony/framework-bundle/tree/v5.4.12" }, "funding": [ { @@ -3117,20 +3118,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-26T10:32:10+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "0a5868e0999e9d47859ba3d918548ff6943e6389" + "reference": "f4bfe9611b113b15d98a43da68ec9b5a00d56791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0a5868e0999e9d47859ba3d918548ff6943e6389", - "reference": "0a5868e0999e9d47859ba3d918548ff6943e6389", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f4bfe9611b113b15d98a43da68ec9b5a00d56791", + "reference": "f4bfe9611b113b15d98a43da68ec9b5a00d56791", "shasum": "" }, "require": { @@ -3142,8 +3143,11 @@ "require-dev": { "predis/predis": "~1.0", "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0" + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" }, "suggest": { "symfony/mime": "To use the file extension guesser" @@ -3174,7 +3178,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.11" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.12" }, "funding": [ { @@ -3190,20 +3194,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-19T07:33:17+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "4fd590a2ef3f62560dbbf6cea511995dd77321ee" + "reference": "37f660fa3bcd78fe4893ce23ebe934618ec099be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4fd590a2ef3f62560dbbf6cea511995dd77321ee", - "reference": "4fd590a2ef3f62560dbbf6cea511995dd77321ee", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/37f660fa3bcd78fe4893ce23ebe934618ec099be", + "reference": "37f660fa3bcd78fe4893ce23ebe934618ec099be", "shasum": "" }, "require": { @@ -3286,7 +3290,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.11" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.12" }, "funding": [ { @@ -3302,7 +3306,7 @@ "type": "tidelift" } ], - "time": "2022-07-29T12:30:22+00:00" + "time": "2022-08-26T14:40:40+00:00" }, { "name": "symfony/intl", @@ -4200,16 +4204,16 @@ }, { "name": "symfony/string", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322" + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5eb661e49ad389e4ae2b6e4df8d783a8a6548322", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322", + "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", "shasum": "" }, "require": { @@ -4266,7 +4270,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.11" + "source": "https://github.com/symfony/string/tree/v5.4.12" }, "funding": [ { @@ -4282,7 +4286,7 @@ "type": "tidelift" } ], - "time": "2022-07-24T16:15:25+00:00" + "time": "2022-08-12T17:03:11+00:00" }, { "name": "symfony/translation-contracts", @@ -4364,16 +4368,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "63b8a50d48c9fe3d04e77307d4f1771dd848baa8" + "reference": "94c3b38514c953e3e84719c96d4e578a01ca1819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/63b8a50d48c9fe3d04e77307d4f1771dd848baa8", - "reference": "63b8a50d48c9fe3d04e77307d4f1771dd848baa8", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/94c3b38514c953e3e84719c96d4e578a01ca1819", + "reference": "94c3b38514c953e3e84719c96d4e578a01ca1819", "shasum": "" }, "require": { @@ -4465,7 +4469,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v5.4.11" + "source": "https://github.com/symfony/twig-bridge/tree/v5.4.12" }, "funding": [ { @@ -4481,7 +4485,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-03T13:09:21+00:00" }, { "name": "symfony/var-dumper", @@ -4647,16 +4651,16 @@ }, { "name": "symfony/yaml", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "05d4ea560f3402c6c116afd99fdc66e60eda227e" + "reference": "7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/05d4ea560f3402c6c116afd99fdc66e60eda227e", - "reference": "05d4ea560f3402c6c116afd99fdc66e60eda227e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c", + "reference": "7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c", "shasum": "" }, "require": { @@ -4702,7 +4706,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.11" + "source": "https://github.com/symfony/yaml/tree/v5.4.12" }, "funding": [ { @@ -4718,7 +4722,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2022-08-02T15:52:22+00:00" }, { "name": "twig/intl-extra", @@ -5484,16 +5488,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -5534,9 +5538,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "openlss/lib-array2xml", @@ -5862,92 +5866,25 @@ }, "time": "2022-03-15T21:29:03+00:00" }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.14", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -5996,7 +5933,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -6004,7 +5941,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -6249,16 +6186,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.21", + "version": "9.5.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", "shasum": "" }, "require": { @@ -6273,7 +6210,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -6281,19 +6217,16 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { "ext-soap": "*", "ext-xdebug": "*" @@ -6335,7 +6268,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" }, "funding": [ { @@ -6345,9 +6278,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-06-19T12:14:25+00:00" + "time": "2022-09-25T03:44:45+00:00" }, { "name": "sebastian/cli-parser", @@ -6518,16 +6455,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -6580,7 +6517,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -6588,7 +6525,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -6778,16 +6715,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -6843,7 +6780,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -6851,7 +6788,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -7206,16 +7143,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { @@ -7227,7 +7164,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -7250,7 +7187,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" }, "funding": [ { @@ -7258,7 +7195,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", @@ -7561,16 +7498,16 @@ }, { "name": "vimeo/psalm", - "version": "4.26.0", + "version": "4.27.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "6998fabb2bf528b65777bf9941920888d23c03ac" + "reference": "faf106e717c37b8c81721845dba9de3d8deed8ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/6998fabb2bf528b65777bf9941920888d23c03ac", - "reference": "6998fabb2bf528b65777bf9941920888d23c03ac", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/faf106e717c37b8c81721845dba9de3d8deed8ff", + "reference": "faf106e717c37b8c81721845dba9de3d8deed8ff", "shasum": "" }, "require": { @@ -7662,9 +7599,9 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.26.0" + "source": "https://github.com/vimeo/psalm/tree/4.27.0" }, - "time": "2022-07-31T13:10:26+00:00" + "time": "2022-08-31T13:47:09+00:00" }, { "name": "webmozart/path-util", diff --git a/config-templates/module_accounting.php b/config-templates/module_accounting.php index 0c00e1055b3b39d7af18c79b0b2f3c5de5098293..4a3538ae0f7aea1d06bf52bea6b2405553e2c0c4 100644 --- a/config-templates/module_accounting.php +++ b/config-templates/module_accounting.php @@ -3,7 +3,9 @@ declare(strict_types=1); use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers; use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; $config = [ /** @@ -12,42 +14,102 @@ $config = [ * * Examples: * urn:oasis:names:tc:SAML:attribute:subject-id - * eduPersonUniqueId + * eduPersonTargetedID * eduPersonPrincipalName + * eduPersonUniqueID */ - ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE => 'urn:oasis:names:tc:SAML:attribute:subject-id', + ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME => 'urn:oasis:names:tc:SAML:attribute:subject-id', + + /** + * Default authentication source which will be used when authenticating users in SimpleSAMLphp Profile Page. + */ + ModuleConfiguration::OPTION_DEFAULT_AUTHENTICATION_SOURCE => 'default-sp', /** * Accounting processing type. There are two possible types: 'synchronous' and 'asynchronous'. - * - 'synchronous': accounting processing will be performed during authentication itself (slower) - * - 'asynchronous': for each authentication event a new job will be created for later processing (faster, - * but requires setting up job storage and a cron entry). */ ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => + /** + * Synchronous option, meaning accounting processing will be performed during authentication itself + * (slower authentication). + */ ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS, + /** + * Asynchronous option, meaning for each authentication event a new job will be created for later processing + * (faster authentication, but requires setting up job storage and a cron entry). + */ + //ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS, /** - * Jobs store. Determines which of the available stores will be used to store jobs in case the 'asynchronous' - * accounting processing type was set. + * Jobs store class. In case of the 'asynchronous' accounting processing type, this determines which class + * will be used to store jobs. The class must implement Stores\Interfaces\JobsStoreInterface. */ - ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\JobsStore::class, + ModuleConfiguration::OPTION_JOBS_STORE => + /** + * Default jobs store class which expects Doctrine DBAL compatible connection to be set below. + */ + Stores\Jobs\DoctrineDbal\Store::class, + + /** + * Default data tracker and provider to be used for accounting and as a source for data display in SSP UI. + * This class must implement Trackers\Interfaces\AuthenticationDataTrackerInterface and + * Providers\Interfaces\AuthenticationDataProviderInterface + */ + ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => + /** + * Track each authentication event for idp / sp / user combination, and any change in idp / sp metadata or + * released user attributes. Each authentication event record will have data used and released at the + * time of the authentication event (versioned idp / sp / user data). This tracker can also be + * used as an authentication data provider. It expects Doctrine DBAL compatible connection + * to be set below. Internally it uses store class + * Stores\Data\DoctrineDbal\DoctrineDbal\Versioned\Store::class. + */ + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class, + /** - * Store connection for particular store. Can be used to set different connections for different stores. + * Additional trackers to run besides default data tracker. These trackers will typically only process and + * persist authentication data to proper data store, and won't be used to display data in SSP UI. + * These tracker classes must implement Trackers\Interfaces\AuthenticationDataTrackerInterface. */ - ModuleConfiguration::OPTION_STORE_TO_CONNECTION_KEY_MAP => [ - Stores\Jobs\DoctrineDbal\JobsStore::class => 'doctrine_dbal_pdo_mysql', + ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ + // TODO mivanci at least one more tracker and its connection + // tracker-class ], /** - * Store connections and their parameters. - * - * Any compatible Doctrine DBAL implementation can be used: - * https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html - * There are additional parameters for: table prefix. - * Examples for mysql and sqlite are provided below. + * Map of classes (stores, trackers, providers, ...) and connection keys, which defines which connections will + * be used. Value for connection key can be string, or it can be an array with two connection types as keys: + * master or slave. Master connection is single connection which will be used to write data to, and it + * must be set. If no slave connections are set, master will also be used to read data from. Slave + * connections are defined as array of strings. If slave connections are set, random one will + * be picked to read data from. + */ + ModuleConfiguration::OPTION_CLASS_TO_CONNECTION_MAP => [ + /** + * Connection key to be used by jobs store class. + */ + Stores\Jobs\DoctrineDbal\Store::class => 'doctrine_dbal_pdo_mysql', + /** + * Connection key to be used by this data tracker and provider. + */ + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class => [ + ModuleConfiguration\ConnectionType::MASTER => 'doctrine_dbal_pdo_mysql', + ModuleConfiguration\ConnectionType::SLAVE => [ + 'doctrine_dbal_pdo_mysql', + ], + ], + ], + + /** + * Connections and their parameters. */ - ModuleConfiguration::OPTION_ALL_STORE_CONNECTIONS_AND_PARAMETERS => [ + ModuleConfiguration::OPTION_CONNECTIONS_AND_PARAMETERS => [ + /** + * Examples for Doctrine DBAL compatible mysql and sqlite connection parameters are provided below (more info + * on https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html). + * There are additional parameters for: table prefix. + */ 'doctrine_dbal_pdo_mysql' => [ 'driver' => 'pdo_mysql', // (string): The built-in driver implementation to use 'user' => 'user', // (string): Username to use when connecting to the database. @@ -58,7 +120,7 @@ $config = [ //'unix_socket' => 'unix_socet', // (string): Name of the socket used to connect to the database. 'charset' => 'utf8', // (string): The charset used when connecting to the database. //'url' => 'mysql://user:secret@localhost/mydb?charset=utf8', // ...alternative way of providing parameters. - // Additional parameters not originaly avaliable in Doctrine DBAL + // Additional parameters not originally available in Doctrine DBAL 'table_prefix' => '', // (string): Prefix for each table. ], 'doctrine_dbal_pdo_sqlite' => [ @@ -69,7 +131,7 @@ $config = [ // Mutually exclusive with path. path takes precedence. //'url' => 'sqlite:////path/to/db.sqlite // ...alternative way of providing path parameter. //'url' => 'sqlite:///:memory:' // ...alternative way of providing memory parameter. - // Additional parameters not originaly avaliable in Doctrine DBAL + // Additional parameters not originally available in Doctrine DBAL 'table_prefix' => '', // (string): Prefix for each table. ], ], diff --git a/psalm.xml b/psalm.xml index d95e9396c08954783a357a3e030d84280eed6870..87c90075a861695c2d56c923737061571b81524e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -23,6 +23,7 @@ <errorLevel type="suppress"> <directory name="config-templates" /> <directory name="tests/config-templates" /> + <directory name="tests/attributemap" /> </errorLevel> </UnusedVariable> diff --git a/routing/routes/routes.yml b/routing/routes/routes.yml index 9ac5397c558584c1420245aa55f088f957072bfe..6b1972c0ad84a604328e4eb16276f14b0c4a1971 100644 --- a/routing/routes/routes.yml +++ b/routing/routes/routes.yml @@ -1,4 +1,30 @@ -accounting-test: - path: /test - defaults: { _controller: 'SimpleSAML\Module\accounting\Controller\Test::test' } +accounting-setup: + path: /setup + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::setup' } + +accounting-job-runner: + path: /job-runner + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::jobRunner' } + +accounting-admin-configuration-status: + path: /admin/configuration/status + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Admin\Configuration::status' } + +accounting-user-personal-data: + path: /user/personal-data + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\User\Profile::personalData' } + +accounting-user-connected-organizations: + path: /user/connected-organizations + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\User\Profile::connectedOrganizations' } + +accounting-user-activity: + path: /user/activity + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\User\Profile::activity' } + +accounting-user-logout: + path: /user/logout + methods: + - POST + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\User\Profile::logout' } diff --git a/routing/services/services.yml b/routing/services/services.yml index 18958e290e6d8ca6eb8bd80f46db2f76fd8b7b8e..a85987f93c7e37693b49911bbfa5f223faf3d668 100644 --- a/routing/services/services.yml +++ b/routing/services/services.yml @@ -1,7 +1,19 @@ services: # default configuration for services in *this* file _defaults: + autowire: true public: false + bind: + Psr\Log\LoggerInterface: '@accounting.logger' - SimpleSAML\Module\accounting\ModuleConfiguration: - class: SimpleSAML\Module\accounting\ModuleConfiguration \ No newline at end of file + # Services + SimpleSAML\Module\accounting\: + resource: '../../src/*' + exclude: '../../src/{Http/Controllers}' + # Service aliases + accounting.logger: + class: SimpleSAML\Module\accounting\Services\Logger + # Controllers + SimpleSAML\Module\accounting\Http\Controllers\: + resource: '../../src/Http/Controllers/*' + tags: ['controller.service_arguments'] \ No newline at end of file diff --git a/src/Auth/Process/Accounting.php b/src/Auth/Process/Accounting.php new file mode 100644 index 0000000000000000000000000000000000000000..146523a848111bf9fcbdfeefcb425c8a560f8770 --- /dev/null +++ b/src/Auth/Process/Accounting.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Auth\Process; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Auth\ProcessingFilter; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Services\Logger; +use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder; +use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder; +use SimpleSAML\Module\accounting\Trackers\Interfaces\AuthenticationDataTrackerInterface; + +class Accounting extends ProcessingFilter +{ + protected ModuleConfiguration $moduleConfiguration; + protected JobsStoreBuilder $jobsStoreBuilder; + protected LoggerInterface $logger; + protected AuthenticationDataTrackerBuilder $authenticationDataTrackerBuilder; + + /** + * @param array $config + * @param mixed $reserved + * @param ModuleConfiguration|null $moduleConfiguration + * @param LoggerInterface|null $logger + * @param JobsStoreBuilder|null $jobsStoreBuilder + */ + public function __construct( + array &$config, + $reserved, + ModuleConfiguration $moduleConfiguration = null, + LoggerInterface $logger = null, + JobsStoreBuilder $jobsStoreBuilder = null, + AuthenticationDataTrackerBuilder $authenticationDataTrackerBuilder = null + ) { + parent::__construct($config, $reserved); + + $this->moduleConfiguration = $moduleConfiguration ?? new ModuleConfiguration(); + $this->logger = $logger ?? new Logger(); + $this->jobsStoreBuilder = $jobsStoreBuilder ?? new JobsStoreBuilder($this->moduleConfiguration, $this->logger); + + $this->authenticationDataTrackerBuilder = $authenticationDataTrackerBuilder ?? + new AuthenticationDataTrackerBuilder($this->moduleConfiguration, $this->logger); + } + + /** + * @throws StoreException + */ + public function process(array &$state): void + { + try { + $authenticationEvent = new Event(new State($state)); + + if ($this->isAccountingProcessingTypeAsynchronous()) { + // Only create authentication event job for later processing... + $this->createAuthenticationEventJob($authenticationEvent); + return; + } + + // Accounting type is synchronous, so do the processing right away... + $configuredTrackers = array_merge( + [$this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()], + $this->moduleConfiguration->getAdditionalTrackers() + ); + + /** @var string $tracker */ + foreach ($configuredTrackers as $tracker) { + ($this->authenticationDataTrackerBuilder->build($tracker))->process($authenticationEvent); + } + } catch (\Throwable $exception) { + $message = sprintf('Accounting error, skipping... Error was: %s.', $exception->getMessage()); + $this->logger->error($message, $state); + } + } + + protected function isAccountingProcessingTypeAsynchronous(): bool + { + return $this->moduleConfiguration->getAccountingProcessingType() === + ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS; + } + + /** + * @throws StoreException + */ + protected function createAuthenticationEventJob(Event $authenticationEvent): void + { + ($this->jobsStoreBuilder->build($this->moduleConfiguration->getJobsStoreClass())) + ->enqueue(new Event\Job($authenticationEvent)); + } +} diff --git a/src/Controller/Test.php b/src/Controller/Test.php deleted file mode 100644 index 1ef34ed82e9fcd3689c2e14ffd59648693add681..0000000000000000000000000000000000000000 --- a/src/Controller/Test.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace SimpleSAML\Module\accounting\Controller; - -use Exception; -use SimpleSAML\Configuration; -use SimpleSAML\Locale\Translate; -use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Session; -use SimpleSAML\XHTML\Template; -use Symfony\Component\HttpFoundation\Request; - -class Test -{ - protected Configuration $configuration; - protected Session $session; - protected ModuleConfiguration $moduleConfiguration; - - /** - * @param Configuration $configuration - * @param Session $session The current user session. - * @param ModuleConfiguration $moduleConfiguration - */ - public function __construct( - Configuration $configuration, - Session $session, - ModuleConfiguration $moduleConfiguration - ) { - $this->configuration = $configuration; - $this->session = $session; - $this->moduleConfiguration = $moduleConfiguration; - } - - /** - * @param Request $request - * @return Template - * @throws Exception - */ - public function test(Request $request): Template - { - $template = new Template($this->configuration, 'accounting:configuration.twig'); - - $template->data = [ - 'test' => Translate::noop('Accounting'), - 'test_config' => $this->moduleConfiguration->getConfiguration()->getString('test_config'), - ]; - - return $template; - } -} diff --git a/src/Entities/Activity.php b/src/Entities/Activity.php new file mode 100644 index 0000000000000000000000000000000000000000..69699f56670bd73639b05f6d8f3cf430e6d1521e --- /dev/null +++ b/src/Entities/Activity.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities; + +use DateTimeImmutable; + +class Activity +{ + protected ServiceProvider $serviceProvider; + protected User $user; + protected DateTimeImmutable $happenedAt; + + public function __construct( + ServiceProvider $serviceProvider, + User $user, + DateTimeImmutable $happenedAt + ) { + $this->serviceProvider = $serviceProvider; + $this->user = $user; + $this->happenedAt = $happenedAt; + } + + /** + * @return ServiceProvider + */ + public function getServiceProvider(): ServiceProvider + { + return $this->serviceProvider; + } + + /** + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * @return DateTimeImmutable + */ + public function getHappenedAt(): DateTimeImmutable + { + return $this->happenedAt; + } +} diff --git a/src/Entities/Activity/Bag.php b/src/Entities/Activity/Bag.php new file mode 100644 index 0000000000000000000000000000000000000000..7e91a1c9604527a43b3bcb5bf8e68ebc9d881a41 --- /dev/null +++ b/src/Entities/Activity/Bag.php @@ -0,0 +1,20 @@ +<?php + +namespace SimpleSAML\Module\accounting\Entities\Activity; + +use SimpleSAML\Module\accounting\Entities\Activity; + +class Bag +{ + protected array $activities = []; + + public function add(Activity $activity): void + { + $this->activities[] = $activity; + } + + public function getAll(): array + { + return $this->activities; + } +} diff --git a/src/Entities/Authentication/Event.php b/src/Entities/Authentication/Event.php new file mode 100644 index 0000000000000000000000000000000000000000..08c9e8c3c44e9889632b988b00c4ffae982a5305 --- /dev/null +++ b/src/Entities/Authentication/Event.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities\Authentication; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; + +class Event extends AbstractPayload +{ + protected State $state; + protected \DateTimeImmutable $happenedAt; + + public function __construct(State $state, \DateTimeImmutable $happenedAt = null) + { + $this->state = $state; + $this->happenedAt = $happenedAt ?? new \DateTimeImmutable(); + } + + public function getState(): State + { + return $this->state; + } + + public function getHappenedAt(): \DateTimeImmutable + { + return $this->happenedAt; + } +} diff --git a/src/Entities/AuthenticationEvent/Job.php b/src/Entities/Authentication/Event/Job.php similarity index 63% rename from src/Entities/AuthenticationEvent/Job.php rename to src/Entities/Authentication/Event/Job.php index eaa66aba53624337e61f73e3417e25f7e0bac2fa..5cea7d987cba7e579bdf4e7096edf7d8f65d7ecb 100644 --- a/src/Entities/AuthenticationEvent/Job.php +++ b/src/Entities/Authentication/Event/Job.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +namespace SimpleSAML\Module\accounting\Entities\Authentication\Event; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; class Job extends AbstractJob { - public function getPayload(): AuthenticationEvent + public function getPayload(): Event { return $this->validatePayload($this->payload); } @@ -21,10 +21,10 @@ class Job extends AbstractJob $this->payload = $this->validatePayload($payload); } - protected function validatePayload(AbstractPayload $payload): AuthenticationEvent + protected function validatePayload(AbstractPayload $payload): Event { - if (! ($payload instanceof AuthenticationEvent)) { - throw new UnexpectedValueException('AuthenticationEvent Job payload must be of type AuthenticationEvent.'); + if (! ($payload instanceof Event)) { + throw new UnexpectedValueException('Event Job payload must be of type Event.'); } return $payload; diff --git a/src/Entities/Authentication/State.php b/src/Entities/Authentication/State.php new file mode 100644 index 0000000000000000000000000000000000000000..6f0ff682e9169db82241ae27a838b52186af1987 --- /dev/null +++ b/src/Entities/Authentication/State.php @@ -0,0 +1,192 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities\Authentication; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Helpers\NetworkHelper; + +class State +{ + public const KEY_ATTRIBUTES = 'Attributes'; + public const KEY_AUTHENTICATION_INSTANT = 'AuthnInstant'; + public const KEY_IDENTITY_PROVIDER_METADATA = 'IdPMetadata'; + public const KEY_SOURCE = 'Source'; + public const KEY_SERVICE_PROVIDER_METADATA = 'SPMetadata'; + public const KEY_DESTINATION = 'Destination'; + + public const KEY_ACCOUNTING = 'accounting'; + public const ACCOUNTING_KEY_CLIENT_IP_ADDRESS = 'client_ip_address'; + + protected string $identityProviderEntityId; + protected string $serviceProviderEntityId; + protected array $attributes; + protected \DateTimeImmutable $createdAt; + protected ?\DateTimeImmutable $authenticationInstant; + protected array $identityProviderMetadata; + protected array $serviceProviderMetadata; + protected ?string $clientIpAddress; + + public function __construct(array $state, \DateTimeImmutable $createdAt = null) + { + $this->createdAt = $createdAt ?? new \DateTimeImmutable(); + + $this->identityProviderMetadata = $this->resolveIdentityProviderMetadata($state); + $this->identityProviderEntityId = $this->resolveIdentityProviderEntityId(); + $this->serviceProviderMetadata = $this->resolveServiceProviderMetadata($state); + $this->serviceProviderEntityId = $this->resolveServiceProviderEntityId(); + $this->attributes = $this->resolveAttributes($state); + $this->authenticationInstant = $this->resolveAuthenticationInstant($state); + $this->clientIpAddress = $this->resolveClientIpAddress($state); + } + + protected function resolveIdentityProviderEntityId(): string + { + if ( + !empty($this->identityProviderMetadata[AbstractProvider::METADATA_KEY_ENTITY_ID]) && + is_string($this->identityProviderMetadata[AbstractProvider::METADATA_KEY_ENTITY_ID]) + ) { + return $this->identityProviderMetadata[AbstractProvider::METADATA_KEY_ENTITY_ID]; + } + + throw new UnexpectedValueException('IdP metadata array does not contain entity ID.'); + } + + protected function resolveServiceProviderEntityId(): string + { + if ( + !empty($this->serviceProviderMetadata[AbstractProvider::METADATA_KEY_ENTITY_ID]) && + is_string($this->serviceProviderMetadata[AbstractProvider::METADATA_KEY_ENTITY_ID]) + ) { + return $this->serviceProviderMetadata[AbstractProvider::METADATA_KEY_ENTITY_ID]; + } + + throw new UnexpectedValueException('SP metadata array does not contain entity ID.'); + } + + protected function resolveAttributes(array $state): array + { + if (empty($state[self::KEY_ATTRIBUTES]) || !is_array($state[self::KEY_ATTRIBUTES])) { + throw new UnexpectedValueException('State array does not contain user attributes.'); + } + + return $state[self::KEY_ATTRIBUTES]; + } + + public function getIdentityProviderEntityId(): string + { + return $this->identityProviderEntityId; + } + + public function getServiceProviderEntityId(): string + { + return $this->serviceProviderEntityId; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getAttributeValue(string $attributeName): ?string + { + if (!empty($this->attributes[$attributeName]) && is_array($this->attributes[$attributeName])) { + return (string)reset($this->attributes[$attributeName]); + } + + return null; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + protected function resolveAuthenticationInstant(array $state): ?\DateTimeImmutable + { + if (empty($state[self::KEY_AUTHENTICATION_INSTANT])) { + return null; + } + + $authInstant = (string)$state[self::KEY_AUTHENTICATION_INSTANT]; + + try { + return new \DateTimeImmutable('@' . $authInstant); + } catch (\Throwable $exception) { + $message = sprintf( + 'Unable to create DateTimeImmutable using AuthInstant value \'%s\'. Error was: %s.', + $authInstant, + $exception->getMessage() + ); + throw new UnexpectedValueException($message); + } + } + + public function getAuthenticationInstant(): ?\DateTimeImmutable + { + return $this->authenticationInstant; + } + + protected function resolveIdentityProviderMetadata(array $state): array + { + if ( + !empty($state[self::KEY_IDENTITY_PROVIDER_METADATA]) && + is_array($state[self::KEY_IDENTITY_PROVIDER_METADATA]) + ) { + return $state[self::KEY_IDENTITY_PROVIDER_METADATA]; + } elseif (!empty($state[self::KEY_SOURCE]) && is_array($state[self::KEY_SOURCE])) { + return $state[self::KEY_SOURCE]; + } + + throw new UnexpectedValueException('State array does not contain IdP metadata.'); + } + + protected function resolveServiceProviderMetadata(array $state): array + { + if ( + !empty($state[self::KEY_SERVICE_PROVIDER_METADATA]) && + is_array($state[self::KEY_SERVICE_PROVIDER_METADATA]) + ) { + return $state[self::KEY_SERVICE_PROVIDER_METADATA]; + } elseif (!empty($state[self::KEY_DESTINATION]) && is_array($state[self::KEY_DESTINATION])) { + return $state[self::KEY_DESTINATION]; + } + + throw new UnexpectedValueException('State array does not contain SP metadata.'); + } + + /** + * @return array + */ + public function getIdentityProviderMetadata(): array + { + return $this->identityProviderMetadata; + } + + /** + * @return array + */ + public function getServiceProviderMetadata(): array + { + return $this->serviceProviderMetadata; + } + + protected function resolveClientIpAddress(array $state): ?string + { + return NetworkHelper::resolveClientIpAddress( + isset($state[self::KEY_ACCOUNTING][self::ACCOUNTING_KEY_CLIENT_IP_ADDRESS]) ? + (string)$state[self::KEY_ACCOUNTING][self::ACCOUNTING_KEY_CLIENT_IP_ADDRESS] + : null + ); + } + + /** + * @return string|null + */ + public function getClientIpAddress(): ?string + { + return $this->clientIpAddress; + } +} diff --git a/src/Entities/AuthenticationEvent.php b/src/Entities/AuthenticationEvent.php deleted file mode 100644 index 37b349d876ed9519be0b44b3308743e0734a69e2..0000000000000000000000000000000000000000 --- a/src/Entities/AuthenticationEvent.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace SimpleSAML\Module\accounting\Entities; - -use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; - -class AuthenticationEvent extends AbstractPayload -{ - protected array $state; - - public function __construct(array $state) - { - $this->state = $state; - } - - public function getState(): array - { - return $this->state; - } -} diff --git a/src/Entities/Bases/AbstractJob.php b/src/Entities/Bases/AbstractJob.php index a41f3450c2dde05e438296ccd487cc83fd710e12..4b9380ea96efce220b7f9bad05c7fb341e7eede3 100644 --- a/src/Entities/Bases/AbstractJob.php +++ b/src/Entities/Bases/AbstractJob.php @@ -4,22 +4,23 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Entities\Bases; +use DateTimeImmutable; use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; abstract class AbstractJob implements JobInterface { protected AbstractPayload $payload; protected ?int $id; - protected \DateTimeImmutable $createdAt; + protected DateTimeImmutable $createdAt; public function __construct( AbstractPayload $payload, int $id = null, - \DateTimeImmutable $createdAt = null + DateTimeImmutable $createdAt = null ) { $this->setPayload($payload); $this->id = $id; - $this->createdAt = $createdAt ?? new \DateTimeImmutable(); + $this->createdAt = $createdAt ?? new DateTimeImmutable(); } public function getId(): ?int @@ -37,7 +38,7 @@ abstract class AbstractJob implements JobInterface $this->payload = $payload; } - public function getCreatedAt(): \DateTimeImmutable + public function getCreatedAt(): DateTimeImmutable { return $this->createdAt; } diff --git a/src/Entities/Bases/AbstractProvider.php b/src/Entities/Bases/AbstractProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..f96a449db5548180a278537bc3fbc14532c28222 --- /dev/null +++ b/src/Entities/Bases/AbstractProvider.php @@ -0,0 +1,56 @@ +<?php + +namespace SimpleSAML\Module\accounting\Entities\Bases; + +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; + +abstract class AbstractProvider +{ + public const METADATA_KEY_NAME = 'name'; + public const METADATA_KEY_ENTITY_ID = 'entityid'; + + protected array $metadata; + protected string $entityId; + + public function __construct(array $metadata) + { + $this->metadata = $metadata; + $this->entityId = $this->resolveEntityId(); + } + + public function getMetadata(): array + { + return $this->metadata; + } + + public function getName(string $locale = 'en'): ?string + { + if ( + isset($this->metadata[self::METADATA_KEY_NAME]) && + is_array($this->metadata[self::METADATA_KEY_NAME]) && + !empty($this->metadata[self::METADATA_KEY_NAME][$locale]) && + is_string($this->metadata[self::METADATA_KEY_NAME][$locale]) + ) { + return (string)$this->metadata[self::METADATA_KEY_NAME][$locale]; + } + + return null; + } + + public function getEntityId(): string + { + return $this->entityId; + } + + protected function resolveEntityId(): string + { + if ( + !empty($this->metadata[self::METADATA_KEY_ENTITY_ID]) && + is_string($this->metadata[self::METADATA_KEY_ENTITY_ID]) + ) { + return $this->metadata[self::METADATA_KEY_ENTITY_ID]; + } + + throw new UnexpectedValueException('Provider entity metadata does not contain entity ID.'); + } +} diff --git a/src/Entities/ConnectedServiceProvider.php b/src/Entities/ConnectedServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..21bf0fc3a6c618c91faa71233d09e9b04d0ef9e5 --- /dev/null +++ b/src/Entities/ConnectedServiceProvider.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities; + +/** + * Represents a Service Provider to which a user has authenticated at least once. + */ +class ConnectedServiceProvider +{ + protected ServiceProvider $serviceProvider; + protected int $numberOfAuthentications; + protected \DateTimeImmutable $lastAuthenticationAt; + protected \DateTimeImmutable $firstAuthenticationAt; + protected User $user; + + /** + * TODO mivanci make sortable by name (or entity ID if not present), number of authns, last/first authn. + * @param ServiceProvider $serviceProvider + * @param int $numberOfAuthentications + * @param \DateTimeImmutable $lastAuthenticationAt + * @param \DateTimeImmutable $firstAuthenticationAt + * @param User $user + */ + public function __construct( + ServiceProvider $serviceProvider, + int $numberOfAuthentications, + \DateTimeImmutable $lastAuthenticationAt, + \DateTimeImmutable $firstAuthenticationAt, + User $user + ) { + $this->serviceProvider = $serviceProvider; + $this->numberOfAuthentications = $numberOfAuthentications; + $this->lastAuthenticationAt = $lastAuthenticationAt; + $this->firstAuthenticationAt = $firstAuthenticationAt; + $this->user = $user; + } + + /** + * @return ServiceProvider + */ + public function getServiceProvider(): ServiceProvider + { + return $this->serviceProvider; + } + + /** + * @return int + */ + public function getNumberOfAuthentications(): int + { + return $this->numberOfAuthentications; + } + + /** + * @return \DateTimeImmutable + */ + public function getLastAuthenticationAt(): \DateTimeImmutable + { + return $this->lastAuthenticationAt; + } + + /** + * @return \DateTimeImmutable + */ + public function getFirstAuthenticationAt(): \DateTimeImmutable + { + return $this->firstAuthenticationAt; + } + + /** + * @return User + */ + public function getUser(): User + { + return $this->user; + } +} diff --git a/src/Entities/ConnectedServiceProvider/Bag.php b/src/Entities/ConnectedServiceProvider/Bag.php new file mode 100644 index 0000000000000000000000000000000000000000..95b77877c762875f3cb79ccc63eaad02f7d74297 --- /dev/null +++ b/src/Entities/ConnectedServiceProvider/Bag.php @@ -0,0 +1,25 @@ +<?php + +namespace SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; + +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; + +class Bag +{ + /** + * @var ConnectedServiceProvider[] + */ + protected array $connectedServiceProviders = []; + + public function addOrReplace(ConnectedServiceProvider $connectedServiceProvider): void + { + $spEntityId = $connectedServiceProvider->getServiceProvider()->getEntityId(); + + $this->connectedServiceProviders[$spEntityId] = $connectedServiceProvider; + } + + public function getAll(): array + { + return $this->connectedServiceProviders; + } +} diff --git a/src/Entities/GenericJob.php b/src/Entities/GenericJob.php index 6577929bb3a978eae43619ace4a6aa1a69fb14bb..5031ba4a8975203e50691ae615752bf48a173631 100644 --- a/src/Entities/GenericJob.php +++ b/src/Entities/GenericJob.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Entities; class GenericJob extends Bases\AbstractJob diff --git a/src/Entities/IdentityProvider.php b/src/Entities/IdentityProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..3207e918717099fcd26e8681e0855c2b54746c64 --- /dev/null +++ b/src/Entities/IdentityProvider.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; + +class IdentityProvider extends AbstractProvider +{ +} diff --git a/src/Entities/Interfaces/JobInterface.php b/src/Entities/Interfaces/JobInterface.php index 7ebc01969c8676a6e51cad6aba7f87dc71d47e03..9578ea94d1c02611166db1e37ac9e0127d349b58 100644 --- a/src/Entities/Interfaces/JobInterface.php +++ b/src/Entities/Interfaces/JobInterface.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Entities\Interfaces; +use DateTimeImmutable; use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; interface JobInterface @@ -11,9 +12,10 @@ interface JobInterface public function getId(): ?int; public function getPayload(): AbstractPayload; + public function setPayload(AbstractPayload $payload): void; public function getType(): string; - public function getCreatedAt(): \DateTimeImmutable; + public function getCreatedAt(): DateTimeImmutable; } diff --git a/src/Entities/ServiceProvider.php b/src/Entities/ServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..c5696c8f1f5301ac58f3057fc2163ca385534cda --- /dev/null +++ b/src/Entities/ServiceProvider.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; + +class ServiceProvider extends AbstractProvider +{ +} diff --git a/src/Entities/User.php b/src/Entities/User.php new file mode 100644 index 0000000000000000000000000000000000000000..138bc4cb5f99501a15aea7b7fe23ffb4f601d9db --- /dev/null +++ b/src/Entities/User.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities; + +class User +{ + protected array $attributes; + + public function __construct(array $attributes) + { + $this->attributes = $attributes; + } + + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } +} diff --git a/src/Exceptions/Exception.php b/src/Exceptions/Exception.php new file mode 100644 index 0000000000000000000000000000000000000000..7a0306d210c88d215f326f2b591e86df49ed191b --- /dev/null +++ b/src/Exceptions/Exception.php @@ -0,0 +1,7 @@ +<?php + +namespace SimpleSAML\Module\accounting\Exceptions; + +class Exception extends \Exception +{ +} diff --git a/src/Helpers/ArrayHelper.php b/src/Helpers/ArrayHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..a87f81df2bac5e6653608e3cfac7e42c77064e41 --- /dev/null +++ b/src/Helpers/ArrayHelper.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Helpers; + +class ArrayHelper +{ + public static function recursivelySortByKey(array &$array): void + { + /** @psalm-suppress MixedAssignment */ + foreach ($array as &$value) { + if (is_array($value)) { + self::recursivelySortByKey($value); + } + } + + ksort($array); + } +} diff --git a/src/Helpers/AttributesHelper.php b/src/Helpers/AttributesHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..8fc6806d02e6ddc926c12f2ec8a9594543009289 --- /dev/null +++ b/src/Helpers/AttributesHelper.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Helpers; + +class AttributesHelper +{ + /** + * Map files which translate attribute names to (more) user-friendly format. + */ + public const MAP_FILES_TO_NAME = ['facebook2name.php', 'linkedin2name.php', 'oid2name.php', 'openid2name.php', + 'removeurnprefix.php', 'twitter2name.php', 'urn2name.php', 'windowslive2name.php']; + + public static function getMergedAttributeMapForFiles(string $sspBaseDirectory, array $mapFiles): array + { + // This is the variable name used in map files. It is set to empty array by default, but later populated + // by each include of the map file. + $attributemap = []; + + $fullAttributeMap = []; + + /** @var string $mapFile */ + foreach ($mapFiles as $mapFile) { + $mapFilePath = $sspBaseDirectory . 'attributemap' . DIRECTORY_SEPARATOR . $mapFile; + if (! file_exists($mapFilePath)) { + continue; + } + include $mapFilePath; + $fullAttributeMap = array_merge($fullAttributeMap, $attributemap); + } + + return $fullAttributeMap; + } +} diff --git a/src/Helpers/HashHelper.php b/src/Helpers/HashHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..568b8268425511e180e9863ab82e13eb34e89a80 --- /dev/null +++ b/src/Helpers/HashHelper.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Helpers; + +class HashHelper +{ + public static function getSha256(string $data): string + { + return hash('sha256', $data); + } + + public static function getSha256ForArray(array $array): string + { + ArrayHelper::recursivelySortByKey($array); + return self::getSha256(serialize($array)); + } +} diff --git a/src/Helpers/InstanceBuilderUsingModuleConfigurationHelper.php b/src/Helpers/InstanceBuilderUsingModuleConfigurationHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..fdecc9a5718995d12a72ad811e38eeda3101e091 --- /dev/null +++ b/src/Helpers/InstanceBuilderUsingModuleConfigurationHelper.php @@ -0,0 +1,60 @@ +<?php + +namespace SimpleSAML\Module\accounting\Helpers; + +use Psr\Log\LoggerInterface; +use ReflectionMethod; +use SimpleSAML\Module\accounting\Exceptions\Exception; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +class InstanceBuilderUsingModuleConfigurationHelper +{ + /** + * @param class-string $class + * @param ModuleConfiguration $moduleConfiguration + * @param LoggerInterface $logger + * @param array $additionalArguments + * @param string $method + * @return BuildableUsingModuleConfigurationInterface + * @throws Exception + */ + public static function build( + string $class, + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + array $additionalArguments = [], + string $method = BuildableUsingModuleConfigurationInterface::BUILD_METHOD + ): BuildableUsingModuleConfigurationInterface { + try { + self::validateClass($class); + + $allArguments = array_merge([$moduleConfiguration, $logger], $additionalArguments); + + $reflectionMethod = new ReflectionMethod($class, $method); + /** @var BuildableUsingModuleConfigurationInterface $instance */ + $instance = $reflectionMethod->invoke(null, ...$allArguments); + } catch (\Throwable $exception) { + $message = \sprintf( + 'Error building instance using module configuration. Error was: %s.', + $exception->getMessage() + ); + throw new Exception($message, (int)$exception->getCode(), $exception); + } + + return $instance; + } + + protected static function validateClass(string $class): void + { + if (!is_subclass_of($class, BuildableUsingModuleConfigurationInterface::class)) { + $message = sprintf( + 'Class \'%s\' does not implement interface \'%s\'.', + $class, + BuildableUsingModuleConfigurationInterface::class + ); + throw new UnexpectedValueException($message); + } + } +} diff --git a/src/Helpers/NetworkHelper.php b/src/Helpers/NetworkHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..4d033e7fa97e8281e84fc20c9a77e689643dde08 --- /dev/null +++ b/src/Helpers/NetworkHelper.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Helpers; + +class NetworkHelper +{ + public static function resolveClientIpAddress(string $clientIpAddress = null): ?string + { + /** @var string|null $clientIpAddress */ + $clientIpAddress = $clientIpAddress ?? + $_SERVER['HTTP_CLIENT_IP'] ?? + $_SERVER['HTTP_X_FORWARDED_FOR'] ?? + $_SERVER['REMOTE_ADDR'] ?? + null; + + if (!is_string($clientIpAddress)) { + return null; + } + + $ips = explode(',', $clientIpAddress); + + $ip = mb_substr(trim(array_pop($ips)), 0, 45); + + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + + return null; + } +} diff --git a/src/Http/Controllers/Admin/Configuration.php b/src/Http/Controllers/Admin/Configuration.php new file mode 100644 index 0000000000000000000000000000000000000000..7c71d1b47af571cfa4992dfb2da7c1fdb56fbe35 --- /dev/null +++ b/src/Http/Controllers/Admin/Configuration.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Http\Controllers\Admin; + +use Exception; +use Psr\Log\LoggerInterface; +use SimpleSAML\Configuration as SspConfiguration; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder; +use SimpleSAML\Session; +use SimpleSAML\Utils; +use SimpleSAML\Utils\Auth; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\Request; +use Throwable; + +class Configuration +{ + protected SspConfiguration $sspConfiguration; + protected Session $session; + protected LoggerInterface $logger; + protected Utils\Auth $sspAuthUtils; + + public function __construct( + SspConfiguration $sspConfiguration, + Session $session, + LoggerInterface $logger, + Utils\Auth $sspAuthUtils = null + ) { + $this->sspConfiguration = $sspConfiguration; + $this->session = $session; + $this->logger = $logger; + $this->sspAuthUtils = $sspAuthUtils ?? new Utils\Auth(); + + $this->sspAuthUtils->requireAdmin(); + } + + /** + * @param Request $request + * @return Template + * @throws Exception + */ + public function status(Request $request): Template + { + // Instantiate ModuleConfiguration here (instead in constructor) so we can check for validation errors. + $moduleConfiguration = null; + $configurationValidationErrors = null; + $jobsStore = null; + + try { + $moduleConfiguration = new ModuleConfiguration(); + + if ( + $moduleConfiguration->getAccountingProcessingType() === + ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS + ) { + $jobsStore = (new JobsStoreBuilder($moduleConfiguration, $this->logger)) + ->build($moduleConfiguration->getJobsStoreClass()); + } + } catch (Throwable $exception) { + $configurationValidationErrors = $exception->getMessage(); + } + + $templateData = [ + 'moduleConfiguration' => $moduleConfiguration, + 'configurationValidationErrors' => $configurationValidationErrors, + 'jobsStore' => $jobsStore, + ]; + + $template = new Template($this->sspConfiguration, 'accounting:admin/configuration/status.twig'); + + $template->data = $templateData; + return $template; + } +} diff --git a/src/Http/Controllers/Test.php b/src/Http/Controllers/Test.php new file mode 100644 index 0000000000000000000000000000000000000000..26878a8d441f26a44703165e61ad1f1a83469545 --- /dev/null +++ b/src/Http/Controllers/Test.php @@ -0,0 +1,143 @@ +<?php +// phpcs:ignoreFile + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Http\Controllers; + +use Exception; +use Psr\Log\LoggerInterface; +use SimpleSAML\Configuration as SspConfiguration; +use SimpleSAML\Locale\Translate; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; +use SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker; +use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder; +use SimpleSAML\Session; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\Request; + +/** + * TODO mivanci delete this file + * @psalm-suppress all + */ +class Test +{ + protected SspConfiguration $sspConfiguration; + protected Session $session; + protected ModuleConfiguration $moduleConfiguration; + protected LoggerInterface $logger; + + /** + * @param SspConfiguration $sspConfiguration + * @param Session $session The current user session. + * @param ModuleConfiguration $moduleConfiguration + * @param LoggerInterface $logger + */ + public function __construct( + SspConfiguration $sspConfiguration, + Session $session, + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger + ) { + $this->sspConfiguration = $sspConfiguration; + $this->session = $session; + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + } + + /** + * @param Request $request + * @return Template + * @throws Exception + */ + public function setup(Request $request): Template + { + $template = new Template($this->sspConfiguration, 'accounting:test.twig'); + + $jobsStore = (new JobsStoreBuilder($this->moduleConfiguration, $this->logger)) + ->build($this->moduleConfiguration->getJobsStoreClass()); + + // TODO refactor to tracker builder + $dataStoreConnectionKey = $this->moduleConfiguration->getClassConnectionKey(Tracker::class); + $dataStore = Store::build($this->moduleConfiguration, $this->logger, $dataStoreConnectionKey); + + $jobsStoreNeedsSetup = $jobsStore->needsSetup(); + $jobsStoreSetupRan = false; + + $dataStoreNeedsSetup = $dataStore->needsSetup(); + $dataStoreSetupRan = false; + + + if ($jobsStoreNeedsSetup && $request->query->has('setup')) { + $this->logger->info('Jobs Store setup ran.'); + $jobsStore->runSetup(); + $jobsStoreSetupRan = true; + } + + if ($dataStoreNeedsSetup && $request->query->has('setup')) { + $this->logger->info('Data Store setup ran.'); + $dataStore->runSetup(); + $dataStoreSetupRan = true; + } + + $sampleStateArray = ['Responder'=>[0=>'\\SimpleSAML\\Module\\saml\\IdP\\SAML2',1=>'sendResponse',],'\\SimpleSAML\\Auth\\State.exceptionFunc'=>[0=>'\\SimpleSAML\\Module\\saml\\IdP\\SAML2',1=>'handleAuthError',],'\\SimpleSAML\\Auth\\State.restartURL'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/SSOService.php?spentityid=https%3A%2F%2Fpc-example.org.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fsaml%2Fsp%2Fmetadata.php%2Fdefault-sp&RelayState=https%3A%2F%2Flocalhost.someone.from.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fadmin%2Ftest%2Fdefault-sp&cookieTime=1660912195','SPMetadata'=>['SingleLogoutService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp',],],'AssertionConsumerService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>0,],1=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>1,],],'contacts'=>[0=>['emailAddress'=>'example@org.hr','givenName'=>'MarkoIvančić','contactType'=>'technical',],],'entityid'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-index'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-set'=>'saml20-sp-remote',],'saml:RelayState'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp','saml:RequestId'=>null,'saml:IDPList'=>[],'saml:ProxyCount'=>null,'saml:RequesterID'=>null,'ForceAuthn'=>false,'isPassive'=>false,'saml:ConsumerURL'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','saml:Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','saml:NameIDFormat'=>null,'saml:AllowCreate'=>true,'saml:Extensions'=>null,'saml:AuthnRequestReceivedAt'=>1660912195.505402,'saml:RequestedAuthnContext'=>null,'core:IdP'=>'saml2:https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','core:SP'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','IdPMetadata'=>['host'=>'localhost.someone.from.hr','privatekey'=>'key.pem','certificate'=>'cert.pem','auth'=>'example-userpass','attributes.NameFormat'=>'urn:oasis:names:tc:SAML:2.0:attrname-format:uri','authproc'=>[100=>['class'=>'core:AttributeMap',0=>'name2oid',],],'entityid'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-index'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-set'=>'saml20-idp-hosted',],'ReturnCallback'=>[0=>'\\SimpleSAML\\IdP',1=>'postAuth',],'Attributes'=>['hrEduPersonUniqueID'=>[0=>'testuser@primjer2.hr',],'urn:oid:0.9.2342.19200300.100.1.1'=>[0=>'testuser',],'urn:oid:2.5.4.3'=>[0=>'TestNameTestSurname',],'urn:oid:2.5.4.4'=>[0=>'TestSurname',1=>'TestSurname2',],'urn:oid:2.5.4.42'=>[0=>'TestName',],'urn:oid:0.9.2342.19200300.100.1.3'=>[0=>'testusermail@primjer.hr',1=>'testusermail2@primjer.hr',],'urn:oid:2.5.4.20'=>[0=>'123',],'hrEduPersonExtensionNumber'=>[0=>'123',],'urn:oid:0.9.2342.19200300.100.1.41'=>[0=>'123',],'urn:oid:2.5.4.23'=>[0=>'123',],'hrEduPersonUniqueNumber'=>[0=>'LOCAL_NO:100',1=>'OIB:00861590360',],'hrEduPersonOIB'=>[0=>'00861590360',],'hrEduPersonDateOfBirth'=>[0=>'20200101',],'hrEduPersonGender'=>[0=>'9',],'urn:oid:0.9.2342.19200300.100.1.60'=>[0=>'987654321',],'urn:oid:2.5.4.36'=>[0=>'cert-value',],'urn:oid:1.3.6.1.4.1.250.1.57'=>[0=>'http://www.org.hr/~ivekHomePage',],'hrEduPersonProfessionalStatus'=>[0=>'VSS',],'hrEduPersonAcademicStatus'=>[0=>'redovitiprofesor',],'hrEduPersonScienceArea'=>[0=>'sociologija',],'hrEduPersonAffiliation'=>[0=>'djelatnik',1=>'student',],'hrEduPersonPrimaryAffiliation'=>[0=>'djelatnik',],'hrEduPersonStudentCategory'=>[0=>'redovitistudent:diplomskisveučilišnistudij',],'hrEduPersonExpireDate'=>[0=>'20211231',],'hrEduPersonTitle'=>[0=>'rektor',],'hrEduPersonRole'=>[0=>'ICTkoordinator',1=>'ISVUkoordinator',],'hrEduPersonStaffCategory'=>[0=>'nastavnoosoblje',1=>'istraživači',],'hrEduPersonGroupMember'=>[0=>'grupa1',1=>'grupa2',],'urn:oid:2.5.4.10'=>[0=>'Testnaustanova',],'hrEduPersonHomeOrg'=>[0=>'primjer.hr',],'urn:oid:2.5.4.11'=>[0=>'Testnaorgjedinica',],'urn:oid:0.9.2342.19200300.100.1.6'=>[0=>'123',],'urn:oid:2.5.4.16'=>[0=>'Testnaustanova,Baštijanova52A,HR-10000Zagreb',],'urn:oid:2.5.4.7'=>[0=>'Zagreb',],'urn:oid:2.5.4.17'=>[0=>'HR-10000',],'urn:oid:2.5.4.9'=>[0=>'Baštijanova52A',],'urn:oid:0.9.2342.19200300.100.1.39'=>[0=>'Testnaadresa52A,HR-10000,Zagreb',],'urn:oid:0.9.2342.19200300.100.1.20'=>[0=>'123',],'hrEduPersonCommURI'=>[0=>'123',],'hrEduPersonPrivacy'=>[0=>'street',1=>'postalCode',],'hrEduPersonPersistentID'=>[0=>'da4294fb4e5746d57ab6ad88d2daf275',],'urn:oid:2.16.840.1.113730.3.1.241'=>[0=>'testname123',],'urn:oid:1.3.6.1.4.1.25178.1.2.12'=>[0=>'skype:pepe.perez',],'hrEduPersonCardNum'=>[0=>'123',],'updatedAt'=>[0=>'123456789',],'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'=>[0=>'urn:example:oidc:manage:client',],],'Authority'=>'example-userpass','AuthnInstant'=>1660911943,'Expire'=>1660940743,'ReturnCall'=>[0=>'\\SimpleSAML\\IdP',1=>'postAuthProc',],'Destination'=>['SingleLogoutService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp',],],'AssertionConsumerService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>0,],1=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>1,],],'contacts'=>[0=>['emailAddress'=>'example@org.hr','givenName'=>'MarkoIvančić','contactType'=>'technical',],],'entityid'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-index'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-set'=>'saml20-sp-remote',],'Source'=>['host'=>'localhost.someone.from.hr','privatekey'=>'key.pem','certificate'=>'cert.pem','auth'=>'example-userpass','attributes.NameFormat'=>'urn:oasis:names:tc:SAML:2.0:attrname-format:uri','authproc'=>[100=>['class'=>'core:AttributeMap',0=>'name2oid',],],'entityid'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-index'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-set'=>'saml20-idp-hosted',],'\\SimpleSAML\\Auth\\ProcessingChain.filters'=>[],]; + $state = new State($sampleStateArray); + + $authenticationEvent = new Event($state); + + //$dataStore->persist($authenticationEvent); + + $data = [ + 'jobs_store' => $this->moduleConfiguration->getJobsStoreClass(), + 'jobs_store_needs_setup' => $jobsStoreNeedsSetup ? 'yes' : 'no', + 'jobs_store_setup_ran' => $jobsStoreSetupRan ? 'yes' : 'no', + + 'data_store' => get_class($dataStore), + 'data_store_needs_setup' => $dataStoreNeedsSetup ? 'yes' : 'no', + 'data_store_setup_ran' => $dataStoreSetupRan ? 'yes' : 'no', + ]; + + die(var_dump($data)); + + $template->data = $data; + return $template; + } + + public function jobRunner():void + { + $jobsStore = (new JobsStoreBuilder($this->moduleConfiguration, $this->logger)) + ->build($this->moduleConfiguration->getJobsStoreClass()); + + + $trackerBuilder = new AuthenticationDataTrackerBuilder($this->moduleConfiguration, $this->logger); + + $configuredTrackers = array_merge( + [$this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()], + $this->moduleConfiguration->getAdditionalTrackers() + ); + + $start = new \DateTimeImmutable(); + $data = [ + 'jobs_processed' => 0, + 'start_time' => $start->format('Y-m-d H:i:s.u'), + ]; + + /** @var Event\Job $job */ + while (($job = $jobsStore->dequeue(Event\Job::class)) !== null) { + foreach ($configuredTrackers as $tracker) { + ($trackerBuilder->build($tracker))->process($job->getPayload()); + $data['jobs_processed']++; + } + } + + $end = new \DateTimeImmutable(); + $data['end_time'] = $end->format('Y-m-d H:i:s.u'); + $data['duration'] = $end->diff($start)->format('%s seconds, %f microseconds'); + + die(var_dump($data)); + } +} diff --git a/src/Http/Controllers/User/Profile.php b/src/Http/Controllers/User/Profile.php new file mode 100644 index 0000000000000000000000000000000000000000..dfd427db5834e839692b5b17dd34bef6de43aa85 --- /dev/null +++ b/src/Http/Controllers/User/Profile.php @@ -0,0 +1,165 @@ +<?php + +namespace SimpleSAML\Module\accounting\Http\Controllers\User; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Auth\Simple; +use SimpleSAML\Configuration as SspConfiguration; +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\Module; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers\Builders\AuthenticationDataProviderBuilder; +use SimpleSAML\Session; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @psalm-suppress all TODO mivanci remove this psalm suppress after testing + */ +class Profile +{ + protected ModuleConfiguration $moduleConfiguration; + protected SspConfiguration $sspConfiguration; + protected Session $session; + protected LoggerInterface $logger; + protected string $defaultAuthenticationSource; + protected Simple $authSimple; + protected AuthenticationDataProviderBuilder $authenticationDataProviderBuilder; + + /** + * @param ModuleConfiguration $moduleConfiguration + * @param SspConfiguration $sspConfiguration + * @param Session $session The current user session. + * @param LoggerInterface $logger + * @param Simple|null $authSimple + * @param AuthenticationDataProviderBuilder|null $authenticationDataProviderBuilder + */ + public function __construct( + ModuleConfiguration $moduleConfiguration, + SspConfiguration $sspConfiguration, + Session $session, + LoggerInterface $logger, + Simple $authSimple = null, + AuthenticationDataProviderBuilder $authenticationDataProviderBuilder = null + ) { + $this->moduleConfiguration = $moduleConfiguration; + $this->sspConfiguration = $sspConfiguration; + $this->session = $session; + $this->logger = $logger; + + $this->defaultAuthenticationSource = $moduleConfiguration->getDefaultAuthenticationSource(); + $this->authSimple = $authSimple ?? new Simple($this->defaultAuthenticationSource, $sspConfiguration, $session); + + $this->authenticationDataProviderBuilder = $authenticationDataProviderBuilder ?? + new AuthenticationDataProviderBuilder($this->moduleConfiguration, $this->logger); + + // Make sure the end user is authenticated. + $this->authSimple->requireAuth(); + } + + public function personalData(Request $request): Response + { + $normalizedAttributes = []; + + $toNameAttributeMap = $this->prepareToNameAttributeMap(); + + /** + * @var string $name + * @var string[] $value + */ + foreach ($this->authSimple->getAttributes() as $name => $value) { + // Convert attribute names to user-friendly names. + if (array_key_exists($name, $toNameAttributeMap)) { + $name = $toNameAttributeMap[$name]; + } + $normalizedAttributes[$name] = implode('; ', $value); + } + + die(var_dump($normalizedAttributes)); + $template = new Template($this->sspConfiguration, 'accounting:user/personal-data.twig'); + $template->data = compact('normalizedAttributes'); + return $template; + } + + public function connectedOrganizations(Request $request): Template + { + // TODO mivanci make sure to use slave connection for data provider if available + $this->authSimple->requireAuth(); + $attributes = $this->authSimple->getAttributes(); + $idAttributeName = $this->moduleConfiguration->getUserIdAttributeName(); + + if (empty($attributes[$idAttributeName]) || !is_array($attributes[$idAttributeName])) { + $message = sprintf('No identifier %s present in user attributes.', $idAttributeName); + throw new Module\accounting\Exceptions\Exception($message); + } + + $userIdentifier = (string)reset($attributes[$idAttributeName]); + + $authenticationDataProvider = $this->authenticationDataProviderBuilder + ->build($this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()); + + $this->removeDebugDisplayLimits(); + var_dump($authenticationDataProvider->getConnectedServiceProviders($userIdentifier)); + die(); + $data = []; + $template = new Template($this->sspConfiguration, 'accounting:user/personal-data.twig'); + $template->data = compact('data'); + return $template; + } + + public function activity(): Template + { + $this->authSimple->requireAuth(); + $attributes = $this->authSimple->getAttributes(); + $idAttributeName = $this->moduleConfiguration->getUserIdAttributeName(); + + if (empty($attributes[$idAttributeName]) || !is_array($attributes[$idAttributeName])) { + $message = sprintf('No identifier %s present in user attributes.', $idAttributeName); + throw new Module\accounting\Exceptions\Exception($message); + } + + $userIdentifier = (string)reset($attributes[$idAttributeName]); + + $authenticationDataProvider = $this->authenticationDataProviderBuilder + ->build($this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()); + + $this->removeDebugDisplayLimits(); + var_dump($authenticationDataProvider->getActivity($userIdentifier)); + die(); + $data = []; + $template = new Template($this->sspConfiguration, 'accounting:user/personal-data.twig'); + $template->data = compact('data'); + return $template; + } + + public function logout(): Response + { + // TODO mivanci make logout button available using HTTP POST + return new RunnableResponse([$this->authSimple, 'logout'], [$this->getLogoutUrl()]); + } + + protected function getLogoutUrl(): string + { + return $this->sspConfiguration->getBasePath() . 'logout.php'; + } + + /** + * Load all attribute map files which translate attribute names to user-friendly name format. + */ + protected function prepareToNameAttributeMap(): array + { + return Module\accounting\Helpers\AttributesHelper::getMergedAttributeMapForFiles( + $this->sspConfiguration->getBaseDir(), + Module\accounting\Helpers\AttributesHelper::MAP_FILES_TO_NAME + ); + } + + /** TODO mivanci remove after debugging */ + protected function removeDebugDisplayLimits(): void + { + ini_set('xdebug.var_display_max_depth', -1); + ini_set('xdebug.var_display_max_children', -1); + ini_set('xdebug.var_display_max_data', -1); + } +} diff --git a/src/Interfaces/BuildableUsingModuleConfigurationInterface.php b/src/Interfaces/BuildableUsingModuleConfigurationInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c1a4aedc646a07c1abbd697f4b25e493c8480647 --- /dev/null +++ b/src/Interfaces/BuildableUsingModuleConfigurationInterface.php @@ -0,0 +1,13 @@ +<?php + +namespace SimpleSAML\Module\accounting\Interfaces; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +interface BuildableUsingModuleConfigurationInterface +{ + public const BUILD_METHOD = 'build'; + + public static function build(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger): self; +} diff --git a/src/Interfaces/SetupableInterface.php b/src/Interfaces/SetupableInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..da036e145c3d1ea6d3af16f6d92aa60ad77cd70b --- /dev/null +++ b/src/Interfaces/SetupableInterface.php @@ -0,0 +1,9 @@ +<?php + +namespace SimpleSAML\Module\accounting\Interfaces; + +interface SetupableInterface +{ + public function needsSetup(): bool; + public function runSetup(): void; +} diff --git a/src/ModuleConfiguration.php b/src/ModuleConfiguration.php index 1dcbe138067458f6c3481fd5e4cef86cc91aa036..6f4cce1671cd10baae8d89beb12bea8e5a1a1a89 100644 --- a/src/ModuleConfiguration.php +++ b/src/ModuleConfiguration.php @@ -7,6 +7,12 @@ namespace SimpleSAML\Module\accounting; use Exception; use SimpleSAML\Configuration; use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; +use SimpleSAML\Module\accounting\ModuleConfiguration\AccountingProcessingType; +use SimpleSAML\Module\accounting\ModuleConfiguration\ConnectionType; +use SimpleSAML\Module\accounting\Providers\Interfaces\AuthenticationDataProviderInterface; +use SimpleSAML\Module\accounting\Stores\Interfaces\JobsStoreInterface; +use SimpleSAML\Module\accounting\Trackers\Interfaces\AuthenticationDataTrackerInterface; +use Throwable; class ModuleConfiguration { @@ -15,11 +21,14 @@ class ModuleConfiguration */ public const FILE_NAME = 'module_accounting.php'; - public const OPTION_USER_ID_ATTRIBUTE = 'user_id_attribute'; + public const OPTION_USER_ID_ATTRIBUTE_NAME = 'user_id_attribute_name'; + public const OPTION_DEFAULT_AUTHENTICATION_SOURCE = 'default_authentication_source'; public const OPTION_ACCOUNTING_PROCESSING_TYPE = 'accounting_processing_type'; public const OPTION_JOBS_STORE = 'jobs_store'; - public const OPTION_ALL_STORE_CONNECTIONS_AND_PARAMETERS = 'all_store_connection_and_parameters'; - public const OPTION_STORE_TO_CONNECTION_KEY_MAP = 'store_to_connection_key_map'; + public const OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER = 'default_data_tracker_and_provider'; + public const OPTION_ADDITIONAL_TRACKERS = 'additional_trackers'; + public const OPTION_CONNECTIONS_AND_PARAMETERS = 'connections_and_parameters'; + public const OPTION_CLASS_TO_CONNECTION_MAP = 'class_to_connection_map'; /** * Contains configuration from module configuration file. @@ -34,6 +43,48 @@ class ModuleConfiguration $fileName = $fileName ?? self::FILE_NAME; $this->configuration = Configuration::getConfig($fileName); + + $this->validate(); + } + + public function getAccountingProcessingType(): string + { + return $this->getConfiguration()->getString(self::OPTION_ACCOUNTING_PROCESSING_TYPE); + } + + /** + * Get underlying SimpleSAMLphp Configuration instance. + * + * @return Configuration + */ + public function getConfiguration(): Configuration + { + return $this->configuration; + } + + public function getJobsStoreClass(): string + { + return $this->getConfiguration()->getString(self::OPTION_JOBS_STORE); + } + + public function getDefaultDataTrackerAndProviderClass(): string + { + return $this->getConfiguration()->getString(self::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER); + } + + public function getConnectionsAndParameters(): array + { + return $this->getConfiguration()->getArray(self::OPTION_CONNECTIONS_AND_PARAMETERS); + } + + public function getAdditionalTrackers(): array + { + return $this->getConfiguration()->getArray(self::OPTION_ADDITIONAL_TRACKERS); + } + + public function getClassToConnectionsMap(): array + { + return $this->getConfiguration()->getArray(self::OPTION_CLASS_TO_CONNECTION_MAP); } /** @@ -44,7 +95,7 @@ class ModuleConfiguration */ public function get(string $option) { - if (! $this->configuration->hasValue($option)) { + if (!$this->configuration->hasValue($option)) { throw new InvalidConfigurationException( sprintf('Configuration option does not exist (%s).', $option) ); @@ -53,50 +104,85 @@ class ModuleConfiguration return $this->configuration->getValue($option); } - /** - * Get underlying SimpleSAMLphp Configuration instance. - * - * @return Configuration - */ - public function getConfiguration(): Configuration + public function getUserIdAttributeName(): string { - return $this->configuration; + return $this->getConfiguration()->getString(self::OPTION_USER_ID_ATTRIBUTE_NAME); + } + + public function getDefaultAuthenticationSource(): string + { + return $this->getConfiguration()->getString(self::OPTION_DEFAULT_AUTHENTICATION_SOURCE); } - public function getStoreConnection(string $store): string + public function getClassConnectionKey(string $class, string $connectionType = ConnectionType::MASTER): string { - $connectionMap = $this->getStoreToConnectionMap(); + $this->validateConnectionType($connectionType); + + $connections = $this->getClassToConnectionsMap(); + + if (!isset($connections[$class])) { + throw new InvalidConfigurationException(sprintf('Connection for class \'%s\' not set.', $class)); + } + + $connectionValue = $connections[$class]; + + // If the key is defined directly, return that. + if (is_string($connectionValue)) { + return $connectionValue; + } - if (! isset($connectionMap[$store])) { + if (!is_array($connectionValue)) { throw new InvalidConfigurationException( - sprintf('Connection for store %s is not set.', $store) + sprintf('Connection for class \'%s\' is not defined as string nor as array.', $class) ); } - return (string) $connectionMap[$store]; - } + if (!isset($connectionValue[ConnectionType::MASTER])) { + $message = sprintf( + 'Connection for class \'%s\' is defined as array, however no master connection key is set.', + $class + ); + throw new InvalidConfigurationException($message); + } - public function getStoreToConnectionMap(): array - { - return $this->getConfiguration()->getArray(self::OPTION_STORE_TO_CONNECTION_KEY_MAP); + // By default, use master connection key. + $connectionKey = (string)$connectionValue[ConnectionType::MASTER]; + + if ($connectionType === ConnectionType::MASTER || (! isset($connectionValue[ConnectionType::SLAVE]))) { + return $connectionKey; + } + + if (is_array($connectionValue[ConnectionType::SLAVE])) { + // Return random slave connection key. + $slaveConnections = $connectionValue[ConnectionType::SLAVE]; + $connectionKey = (string)$slaveConnections[array_rand($slaveConnections)]; + } + + return $connectionKey; } - public function getAllStoreConnectionsAndParameters(): array + /** + * @throws InvalidConfigurationException + */ + public function getClassConnectionParameters(string $class, string $connectionType = ConnectionType::MASTER): array { - return $this->getConfiguration()->getArray(self::OPTION_ALL_STORE_CONNECTIONS_AND_PARAMETERS); + return $this->getConnectionParameters($this->getClassConnectionKey($class, $connectionType)); } - public function getStoreConnectionParameters(string $connection): array + /** + * @throws InvalidConfigurationException + */ + public function getConnectionParameters(string $connectionKey): array { - $connections = $this->getAllStoreConnectionsAndParameters(); + $connections = $this->getConnectionsAndParameters(); - if (! isset($connections[$connection]) || ! is_array($connections[$connection])) { + if (!isset($connections[$connectionKey]) || !is_array($connections[$connectionKey])) { throw new InvalidConfigurationException( - sprintf('Settings for connection %s not set', $connection) + sprintf('Connection parameters not set for key \'%s\'.', $connectionKey) ); } - return $connections[$connection]; + return $connections[$connectionKey]; } public function getModuleSourceDirectory(): string @@ -108,4 +194,186 @@ class ModuleConfiguration { return dirname(__DIR__); } + + /** + * @throws InvalidConfigurationException + */ + protected function validate(): void + { + $errors = []; + + try { + $this->validateAccountingProcessingType(); + } catch (Throwable $exception) { + $errors[] = $exception->getMessage(); + } + + // If accounting processing type is async, validate jobs store class and connection. + if ($this->getAccountingProcessingType() === AccountingProcessingType::VALUE_ASYNCHRONOUS) { + try { + $this->validateJobsStoreClass(); + } catch (Throwable $exception) { + $errors[] = $exception->getMessage(); + } + } + + try { + $this->validateDefaultDataTrackerAndProvider(); + } catch (Throwable $exception) { + $errors[] = $exception->getMessage(); + } + + try { + $this->validateAdditionalTrackers(); + } catch (Throwable $exception) { + $errors[] = $exception->getMessage(); + } + + try { + $this->validateClassToConnectionMap(); + } catch (Throwable $exception) { + $errors[] = $exception->getMessage(); + } + + if (!empty($errors)) { + $message = sprintf('Module configuration validation failed with errors: %s', implode(' ', $errors)); + throw new InvalidConfigurationException($message); + } + } + + /** + * @throws InvalidConfigurationException + */ + protected function validateDefaultDataTrackerAndProvider(): void + { + $errors = []; + + // Default data tracker and provider must implement proper interfaces. + $defaultDataTrackerAndProviderClass = $this->getDefaultDataTrackerAndProviderClass(); + + if (!is_subclass_of($defaultDataTrackerAndProviderClass, AuthenticationDataTrackerInterface::class)) { + $errors[] = sprintf( + 'Default authentication data tracker and provider class \'%s\' does not implement interface \'%s\'.', + $defaultDataTrackerAndProviderClass, + AuthenticationDataTrackerInterface::class + ); + } + + if (!is_subclass_of($defaultDataTrackerAndProviderClass, AuthenticationDataProviderInterface::class)) { + $errors[] = sprintf( + 'Default authentication data tracker and provider class \'%s\' does not implement interface \'%s\'.', + $defaultDataTrackerAndProviderClass, + AuthenticationDataProviderInterface::class + ); + } + + if (!empty($errors)) { + throw new InvalidConfigurationException(implode(' ', $errors)); + } + } + + /** + * @throws InvalidConfigurationException + */ + protected function validateAdditionalTrackers(): void + { + $errors = []; + + // Validate additional trackers + /** + * @var string $trackerClass + */ + foreach ($this->getAdditionalTrackers() as $trackerClass) { + /** @psalm-suppress DocblockTypeContradiction */ + if (!is_string($trackerClass)) { + $errors[] = 'Additional trackers array must contain class strings only.'; + } elseif (!is_subclass_of($trackerClass, AuthenticationDataTrackerInterface::class)) { + $errors[] = sprintf( + 'Tracker class \'%s\' does not implement interface \'%s\'.', + $trackerClass, + AuthenticationDataTrackerInterface::class + ); + } + } + + if (!empty($errors)) { + throw new InvalidConfigurationException(implode(' ', $errors)); + } + } + + /** + * @throws InvalidConfigurationException + */ + protected function validateConnectionType(string $connectionType): void + { + if (!in_array($connectionType, ConnectionType::VALID_OPTIONS)) { + $message = sprintf( + 'Connection type \'%s\' is not valid. Possible values are: %s.', + $connectionType, + implode(', ', ConnectionType::VALID_OPTIONS) + ); + throw new InvalidConfigurationException($message); + } + } + + /** + * @throws InvalidConfigurationException + */ + protected function validateAccountingProcessingType(): void + { + // Only defined accounting processing types are allowed. + if (!in_array($this->getAccountingProcessingType(), AccountingProcessingType::VALID_OPTIONS)) { + $message = sprintf( + 'Accounting processing type is not valid; possible values are: %s.', + implode(', ', AccountingProcessingType::VALID_OPTIONS) + ); + + throw new InvalidConfigurationException($message); + } + } + + /** + * @throws InvalidConfigurationException + */ + protected function validateJobsStoreClass(): void + { + // Jobs store class must implement JobsStoreInterface + $jobsStore = $this->getJobsStoreClass(); + if (!class_exists($jobsStore) || !is_subclass_of($jobsStore, JobsStoreInterface::class)) { + $message = sprintf( + 'Provided jobs store class \'%s\' does not implement interface \'%s\'.', + $jobsStore, + JobsStoreInterface::class + ); + + throw new InvalidConfigurationException($message); + } + } + + /** + * @throws InvalidConfigurationException + */ + protected function validateClassToConnectionMap(): void + { + $errors = []; + + $connectionsAndParameters = $this->getConnectionsAndParameters(); + // Each defined class should have defined connection parameters. + $classToConnectionMap = array_keys($this->getClassToConnectionsMap()); + /** @var string $class */ + foreach ($classToConnectionMap as $class) { + $connectionKey = $this->getClassConnectionKey($class); + if (! array_key_exists($connectionKey, $connectionsAndParameters)) { + $errors[] = sprintf( + 'Class \'%s\' has connection key \'%s\' set, however parameters for that key are not set.', + $class, + $connectionKey + ); + } + } + + if (!empty($errors)) { + throw new InvalidConfigurationException(implode(' ', $errors)); + } + } } diff --git a/src/ModuleConfiguration/AccountingProcessingType.php b/src/ModuleConfiguration/AccountingProcessingType.php index 53c88fe84b6178a04b461bb6f24e9e99fd231059..fef0718ff35309d446ce2120ccaa9655840ef3ca 100644 --- a/src/ModuleConfiguration/AccountingProcessingType.php +++ b/src/ModuleConfiguration/AccountingProcessingType.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\ModuleConfiguration; -class AccountingProcessingType +final class AccountingProcessingType { public const VALUE_SYNCHRONOUS = 'synchronous'; public const VALUE_ASYNCHRONOUS = 'asynchronous'; diff --git a/src/ModuleConfiguration/ConnectionType.php b/src/ModuleConfiguration/ConnectionType.php new file mode 100644 index 0000000000000000000000000000000000000000..0ad53b8acb85e98bd4cae27046edc6a12b73c19a --- /dev/null +++ b/src/ModuleConfiguration/ConnectionType.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\ModuleConfiguration; + +final class ConnectionType +{ + public const MASTER = 'master'; + public const SLAVE = 'slave'; + + public const VALID_OPTIONS = [ + self::MASTER, + self::SLAVE, + ]; +} diff --git a/src/Providers/Builders/AuthenticationDataProviderBuilder.php b/src/Providers/Builders/AuthenticationDataProviderBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..a8b2bc374d1ebd60e26e15f651a48b14d531a856 --- /dev/null +++ b/src/Providers/Builders/AuthenticationDataProviderBuilder.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Providers\Builders; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\Exception; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers\Interfaces\AuthenticationDataProviderInterface; +use Throwable; + +class AuthenticationDataProviderBuilder +{ + protected ModuleConfiguration $moduleConfiguration; + protected LoggerInterface $logger; + + public function __construct(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger) + { + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + } + + /** + * @throws Exception + */ + public function build(string $class): AuthenticationDataProviderInterface + { + try { + // Make sure that the class implements proper interface + if (!is_subclass_of($class, AuthenticationDataProviderInterface::class)) { + $message = sprintf( + 'Class %s does not implement interface %s.', + $class, + AuthenticationDataProviderInterface::class + ); + throw new UnexpectedValueException($message); + } + + // Build... + /** @var AuthenticationDataProviderInterface $store */ + $store = InstanceBuilderUsingModuleConfigurationHelper::build( + $class, + $this->moduleConfiguration, + $this->logger + ); + } catch (Throwable $exception) { + $message = sprintf('Error building instance for class %s. Error was: %s', $class, $exception->getMessage()); + throw new Exception($message, (int)$exception->getCode(), $exception); + } + + return $store; + } +} diff --git a/src/Providers/Interfaces/AuthenticationDataProviderInterface.php b/src/Providers/Interfaces/AuthenticationDataProviderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..b5e52c0896321c541e7e00759fcf9b2ce88e6bf2 --- /dev/null +++ b/src/Providers/Interfaces/AuthenticationDataProviderInterface.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Providers\Interfaces; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Activity; +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +interface AuthenticationDataProviderInterface extends BuildableUsingModuleConfigurationInterface +{ + public static function build(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger): self; + + public function getConnectedServiceProviders(string $userIdentifier): ConnectedServiceProvider\Bag; + + public function getActivity(string $userIdentifier): Activity\Bag; +} diff --git a/src/Services/LoggerService.php b/src/Services/Logger.php similarity index 60% rename from src/Services/LoggerService.php rename to src/Services/Logger.php index 11827f959d2664fa76a4cc194198ecb8259ca1c3..c26c296ba52fbbedcfe722bc0a007cca795cdf8c 100644 --- a/src/Services/LoggerService.php +++ b/src/Services/Logger.php @@ -6,40 +6,42 @@ namespace SimpleSAML\Module\accounting\Services; use Psr\Log\AbstractLogger; use Psr\Log\LogLevel; -use SimpleSAML\Logger; +use SimpleSAML\Logger as SspLogger; -class LoggerService extends AbstractLogger +class Logger extends AbstractLogger { - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = [], string $prefix = '(accounting) ') { + $message = $prefix . $message; + if (! empty($context)) { $message .= ' Context: ' . var_export($context, true); } switch ($level) { case LogLevel::EMERGENCY: - Logger::emergency($message); + SspLogger::emergency($message); break; case LogLevel::CRITICAL: - Logger::critical($message); + SspLogger::critical($message); break; case LogLevel::ALERT: - Logger::alert($message); + SspLogger::alert($message); break; case LogLevel::ERROR: - Logger::error($message); + SspLogger::error($message); break; case LogLevel::WARNING: - Logger::warning($message); + SspLogger::warning($message); break; case LogLevel::NOTICE: - Logger::notice($message); + SspLogger::notice($message); break; case LogLevel::INFO: - Logger::info($message); + SspLogger::info($message); break; case LogLevel::DEBUG: - Logger::debug($message); + SspLogger::debug($message); break; } } @@ -51,6 +53,6 @@ class LoggerService extends AbstractLogger */ public function stats(string $message): void { - Logger::stats($message); + SspLogger::stats($message); } } diff --git a/src/Stores/Bases/DoctrineDbal/AbstractRawEntity.php b/src/Stores/Bases/DoctrineDbal/AbstractRawEntity.php new file mode 100644 index 0000000000000000000000000000000000000000..ac0b534bb0553f66b5360b82eabbd0b657250587 --- /dev/null +++ b/src/Stores/Bases/DoctrineDbal/AbstractRawEntity.php @@ -0,0 +1,51 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal; + +use DateTimeImmutable; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use Throwable; + +abstract class AbstractRawEntity +{ + protected array $rawRow; + protected AbstractPlatform $abstractPlatform; + + public function __construct(array $rawRow, AbstractPlatform $abstractPlatform) + { + $this->rawRow = $rawRow; + $this->abstractPlatform = $abstractPlatform; + + $this->validate($rawRow); + } + + /** + * @throws UnexpectedValueException + */ + abstract protected function validate(array $rawRow): void; + + /** + * @param mixed $value + * @return DateTimeImmutable + */ + protected function resolveDateTimeImmutable($value): DateTimeImmutable + { + try { + /** @var DateTimeImmutable $dateTimeImmutable */ + $dateTimeImmutable = (Type::getType(Types::DATETIME_IMMUTABLE)) + ->convertToPHPValue($value, $this->abstractPlatform); + } catch (Throwable $exception) { + $message = sprintf( + 'Could not create DateTimeImmutable using value %s. Error was: %s.', + var_export($value, true), + $exception->getMessage() + ); + throw new UnexpectedValueException($message, (int)$exception->getCode(), $exception); + } + + return $dateTimeImmutable; + } +} diff --git a/src/Stores/Bases/DoctrineDbal/AbstractStore.php b/src/Stores/Bases/DoctrineDbal/AbstractStore.php new file mode 100644 index 0000000000000000000000000000000000000000..1ff76100f412bcbfb810863b8f53d413307a6de8 --- /dev/null +++ b/src/Stores/Bases/DoctrineDbal/AbstractStore.php @@ -0,0 +1,138 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal; + +use Psr\Log\LoggerInterface; +use ReflectionClass; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\Interfaces\SetupableInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; + +abstract class AbstractStore implements BuildableUsingModuleConfigurationInterface, SetupableInterface +{ + protected ModuleConfiguration $moduleConfiguration; + protected Connection $connection; + protected Migrator $migrator; + protected LoggerInterface $logger; + + /** + * @throws StoreException + */ + public function __construct( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + Factory $connectionFactory, + string $connectionKey = null + ) { + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + + $connectionKey = $connectionKey ?? $moduleConfiguration->getClassConnectionKey($this->getSelfClass()); + $this->connection = $connectionFactory->buildConnection($connectionKey); + $this->migrator = $connectionFactory->buildMigrator($this->connection); + } + + /** + * Get ReflectionClass of current store instance. + * @return ReflectionClass + */ + protected function getReflection(): ReflectionClass + { + return new ReflectionClass($this); + } + + /** + * Get class of the current store instance. + * @return string + */ + protected function getSelfClass(): string + { + return $this->getReflection()->getName(); + } + + protected function getMigrationsNamespace(): string + { + return $this->getSelfClass() . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + } + + protected function areAllMigrationsImplemented(): bool + { + return !$this->migrator->hasNonImplementedMigrationClasses( + $this->getMigrationsDirectory(), + $this->getMigrationsNamespace() + ); + } + + /** + * @throws StoreException + * @throws MigrationException + */ + public function runSetup(): void + { + if ($this->migrator->needsSetup()) { + $this->migrator->runSetup(); + } + + if (!$this->areAllMigrationsImplemented()) { + $this->migrator->runNonImplementedMigrationClasses( + $this->getMigrationsDirectory(), + $this->getMigrationsNamespace() + ); + } + } + + /** + * @throws StoreException + */ + public function needsSetup(): bool + { + // ... if the migrator itself needs setup. + if ($this->migrator->needsSetup()) { + return true; + } + + // ... if Store migrations need to run + if (!$this->areAllMigrationsImplemented()) { + return true; + } + + return false; + } + + /** + * Get migrations directory. + * By default, it will return {...}/Store/Migrations directory. + * @return string + */ + protected function getMigrationsDirectory(): string + { + $reflection = $this->getReflection(); + $storeDirName = dirname($reflection->getFileName()); + $storeShortName = $reflection->getShortName(); + + return $storeDirName . DIRECTORY_SEPARATOR . + $storeShortName . DIRECTORY_SEPARATOR . + AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + } + + /** + * Build store instance. Must be implemented in child classes for proper return store type. + * @param ModuleConfiguration $moduleConfiguration + * @param LoggerInterface $logger + * @param string|null $connectionKey + * @return self + */ + abstract public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self; +} diff --git a/src/Stores/Builders/Bases/AbstractStoreBuilder.php b/src/Stores/Builders/Bases/AbstractStoreBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..adadc9d4640df3a4ef8edda41b5d6dc5da4c1b9c --- /dev/null +++ b/src/Stores/Builders/Bases/AbstractStoreBuilder.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Builders\Bases; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Interfaces\StoreInterface; +use Throwable; + +use function sprintf; +use function is_subclass_of; + +abstract class AbstractStoreBuilder +{ + protected ModuleConfiguration $moduleConfiguration; + protected LoggerInterface $logger; + + public function __construct(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger) + { + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + } + + abstract public function build(string $class, string $connectionKey = null): StoreInterface; + + /** + * @throws StoreException + */ + protected function buildGeneric(string $class, array $additionalArguments = []): StoreInterface + { + try { + // Make sure that the class implements StoreInterface + if (!is_subclass_of($class, StoreInterface::class)) { + throw new StoreException(sprintf('Class %s does not implement StoreInterface.', $class)); + } + + // Build store... + /** @var StoreInterface $store */ + $store = InstanceBuilderUsingModuleConfigurationHelper::build( + $class, + $this->moduleConfiguration, + $this->logger, + $additionalArguments + ); + } catch (Throwable $exception) { + $message = sprintf('Error building store for class %s. Error was: %s', $class, $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + return $store; + } +} diff --git a/src/Stores/Builders/DataStoreBuilder.php b/src/Stores/Builders/DataStoreBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..e9e1689f3c6829bacca7bdf411e1b6053ea02ad8 --- /dev/null +++ b/src/Stores/Builders/DataStoreBuilder.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Builders; + +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Stores\Interfaces\DataStoreInterface; + +class DataStoreBuilder extends Bases\AbstractStoreBuilder +{ + /** + * @throws StoreException + */ + public function build(string $class, string $connectionKey = null): DataStoreInterface + { + if (!is_subclass_of($class, DataStoreInterface::class)) { + throw new StoreException( + sprintf('Class \'%s\' does not implement interface \'%s\'.', $class, DataStoreInterface::class) + ); + } + + /** @var DataStoreInterface $store */ + $store = $this->buildGeneric($class, [$connectionKey]); + + return $store; + } +} diff --git a/src/Stores/Builders/JobsStoreBuilder.php b/src/Stores/Builders/JobsStoreBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..401e524d6e687f9711226792919cbceefc6275d7 --- /dev/null +++ b/src/Stores/Builders/JobsStoreBuilder.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Builders; + +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Stores\Interfaces\JobsStoreInterface; + +use function sprintf; + +class JobsStoreBuilder extends Bases\AbstractStoreBuilder +{ + /** + * @throws StoreException + */ + public function build(string $class, string $connectionKey = null): JobsStoreInterface + { + if (!is_subclass_of($class, JobsStoreInterface::class)) { + throw new StoreException( + sprintf('Class \'%s\' does not implement interface \'%s\'.', $class, JobsStoreInterface::class) + ); + } + + $connectionKey = $connectionKey ?? $this->moduleConfiguration->getClassConnectionKey($class); + + /** @var JobsStoreInterface $store */ + $store = $this->buildGeneric($class, [$connectionKey]); + + return $store; + } +} diff --git a/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php b/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php index d0a8b9497ae5850509ed78b5659291a3e4b98c99..9619f19bb7b4a90d9175a18608930e15f160d7c4 100644 --- a/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php +++ b/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php @@ -30,11 +30,10 @@ abstract class AbstractMigration implements MigrationInterface } } - /** - * @throws MigrationException - */ - protected function throwGenericMigrationException(string $contextDetails, Throwable $throwable): void - { + protected function prepareGenericMigrationException( + string $contextDetails, + Throwable $throwable + ): MigrationException { $message = sprintf( 'There was an error running a migration class %s. Context details: %s. Error was: %s.', static::class, @@ -42,6 +41,28 @@ abstract class AbstractMigration implements MigrationInterface $throwable->getMessage() ); - throw new MigrationException($message, (int) $throwable->getCode(), $throwable); + return new MigrationException($message, (int) $throwable->getCode(), $throwable); + } + + /** + * Prepare prefixed table name which will include table prefix from connection, local table prefix, and table name. + * + * @param string $tableName + * @return string + */ + protected function preparePrefixedTableName(string $tableName): string + { + return $this->connection->preparePrefixedTableName($this->getLocalTablePrefix() . $tableName); + } + + /** + * Get local table prefix (prefix per migration). Empty string by default. Override in particular migration to + * set another local prefix. + * + * @return string + */ + protected function getLocalTablePrefix(): string + { + return ''; } } diff --git a/src/Stores/Connections/DoctrineDbal/Factory.php b/src/Stores/Connections/DoctrineDbal/Factory.php index 45d9c1076bb93e4ac78425ded9c9bfcb57ab48f6..b447dbf13c6b356c6f94343e116c4081b6227a9b 100644 --- a/src/Stores/Connections/DoctrineDbal/Factory.php +++ b/src/Stores/Connections/DoctrineDbal/Factory.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal; use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\ModuleConfiguration; class Factory @@ -20,11 +21,14 @@ class Factory public function buildConnection(string $connectionKey): Connection { - return new Connection($this->moduleConfiguration->getStoreConnectionParameters($connectionKey)); + return new Connection($this->moduleConfiguration->getConnectionParameters($connectionKey)); } - public function buildMigrator(Connection $connection, LoggerInterface $loggerService = null): Migrator + /** + * @throws StoreException + */ + public function buildMigrator(Connection $connection): Migrator { - return new Migrator($connection, $loggerService ?? $this->loggerService); + return new Migrator($connection, $this->loggerService); } } diff --git a/src/Stores/Connections/DoctrineDbal/Migrator.php b/src/Stores/Connections/DoctrineDbal/Migrator.php index e804b2876a76aa72d2e1e11d07361be25ede7c9d..5c7250f2c9b8e80c51c0ee2ddef134acfb8bb742 100644 --- a/src/Stores/Connections/DoctrineDbal/Migrator.php +++ b/src/Stores/Connections/DoctrineDbal/Migrator.php @@ -41,9 +41,9 @@ class Migrator extends AbstractMigrator $this->logger = $logger; try { - $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->schemaManager = ($this->connection->dbal())->createSchemaManager(); } catch (Throwable $exception) { - $message = 'Could not create DBAL schema manager.'; + $message = sprintf('Could not create DBAL schema manager. Error was: %s', $exception->getMessage()); throw new StoreException($message, (int) $exception->getCode(), $exception); } $this->prefixedTableName = $this->connection->preparePrefixedTableName(self::TABLE_NAME); @@ -57,7 +57,11 @@ class Migrator extends AbstractMigrator try { return ! $this->schemaManager->tablesExist([$this->prefixedTableName]); } catch (Throwable $exception) { - $message = sprintf('Could not check table %s existence using schema manager.', $this->prefixedTableName); + $message = sprintf( + 'Could not check table \'%s\' existence using schema manager. Error was:%s', + $this->prefixedTableName, + $exception->getMessage() + ); throw new StoreException($message, (int) $exception->getCode(), $exception); } } diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php new file mode 100644 index 0000000000000000000000000000000000000000..543d007dd9c451614b5284fb08f62d62ecb5f35e --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php @@ -0,0 +1,646 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Activity; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Entities\ServiceProvider; +use SimpleSAML\Module\accounting\Entities\User; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Helpers\HashHelper; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\HashDecoratedState; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawActivity; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawConnectedServiceProvider; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository; +use SimpleSAML\Module\accounting\Stores\Interfaces\DataStoreInterface; + +class Store extends AbstractStore implements DataStoreInterface +{ + protected Repository $repository; + + /** + * @throws StoreException + */ + public function __construct( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + Factory $connectionFactory, + string $connectionKey = null, + Repository $repository = null + ) { + parent::__construct($moduleConfiguration, $logger, $connectionFactory, $connectionKey); + + $this->repository = $repository ?? new Repository($this->connection, $this->logger); + } + + /** + * Build store instance. + * @throws StoreException + */ + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self { + return new self( + $moduleConfiguration, + $logger, + new Factory($moduleConfiguration, $logger), + $connectionKey + ); + } + + /** + * @throws StoreException + */ + public function persist(Event $authenticationEvent): void + { + $hashDecoratedState = new HashDecoratedState($authenticationEvent->getState()); + + $idpId = $this->resolveIdpId($hashDecoratedState); + $idpVersionId = $this->resolveIdpVersionId($idpId, $hashDecoratedState); + $spId = $this->resolveSpId($hashDecoratedState); + $spVersionId = $this->resolveSpVersionId($spId, $hashDecoratedState); + $userId = $this->resolveUserId($hashDecoratedState); + $userVersionId = $this->resolveUserVersionId($userId, $hashDecoratedState); + $idpSpUserVersionId = $this->resolveIdpSpUserVersionId($idpVersionId, $spVersionId, $userVersionId); + + $happenedAt = $authenticationEvent->getHappenedAt(); + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $happenedAt); + } + + /** + * @throws StoreException + */ + protected function resolveIdpId(HashDecoratedState $hashDecoratedState): int + { + $idpEntityIdHashSha256 = $hashDecoratedState->getIdentityProviderEntityIdHashSha256(); + + // Check if it already exists. + try { + $result = $this->repository->getIdp($idpEntityIdHashSha256); + $idpId = $result->fetchOne(); + + if ($idpId !== false) { + return (int)$idpId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving Idp ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertIdp( + $hashDecoratedState->getState()->getIdentityProviderEntityId(), + $idpEntityIdHashSha256 + ); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new IdP, however, continuing in case of race condition. Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getIdp($idpEntityIdHashSha256); + $idpIdNew = $result->fetchOne(); + + if ($idpIdNew !== false) { + return (int)$idpIdNew; + } + + $message = sprintf( + 'Error fetching IdP ID even after insertion for entity ID hash SHA256 %s.', + $idpEntityIdHashSha256 + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving Idp ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + protected function resolveIdpVersionId(int $idpId, HashDecoratedState $hashDecoratedState): int + { + // Check if it already exists. + $idpMetadataArrayHashSha256 = $hashDecoratedState->getIdentityProviderMetadataArrayHashSha256(); + + try { + $result = $this->repository->getIdpVersion($idpId, $idpMetadataArrayHashSha256); + $idpVersionId = $result->fetchOne(); + + if ($idpVersionId !== false) { + return (int)$idpVersionId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving IdP Version ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertIdpVersion( + $idpId, + serialize($hashDecoratedState->getState()->getIdentityProviderMetadata()), + $idpMetadataArrayHashSha256 + ); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new IdP Version, however, continuing in case of race condition. Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getIdpVersion($idpId, $idpMetadataArrayHashSha256); + $idpVersionIdNew = $result->fetchOne(); + + if ($idpVersionIdNew !== false) { + return (int)$idpVersionIdNew; + } + + $message = sprintf( + 'Error fetching IdP ID Version even after insertion for Idp ID %s.', + $idpId + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving Idp Version ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + protected function resolveSpId(HashDecoratedState $hashDecoratedState): int + { + $spEntityIdHashSha256 = $hashDecoratedState->getServiceProviderEntityIdHashSha256(); + + // Check if it already exists. + try { + $result = $this->repository->getSp($spEntityIdHashSha256); + $spId = $result->fetchOne(); + + if ($spId !== false) { + return (int)$spId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving SP ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertSp( + $hashDecoratedState->getState()->getServiceProviderEntityId(), + $spEntityIdHashSha256 + ); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new SP, however, continuing in case of race condition. Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getSp($spEntityIdHashSha256); + $spIdNew = $result->fetchOne(); + + if ($spIdNew !== false) { + return (int)$spIdNew; + } + + $message = sprintf( + 'Error fetching SP ID even after insertion for entity ID hash SHA256 %s.', + $spEntityIdHashSha256 + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving SP ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + protected function resolveSpVersionId(int $spId, HashDecoratedState $hashDecoratedState): int + { + // Check if it already exists. + $spMetadataArrayHashSha256 = $hashDecoratedState->getServiceProviderMetadataArrayHashSha256(); + + try { + $result = $this->repository->getSpVersion($spId, $spMetadataArrayHashSha256); + $spVersionId = $result->fetchOne(); + + if ($spVersionId !== false) { + return (int)$spVersionId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving SP Version ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertSpVersion( + $spId, + serialize($hashDecoratedState->getState()->getServiceProviderMetadata()), + $spMetadataArrayHashSha256 + ); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new SP Version, however, continuing in case of race condition. Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getSpVersion($spId, $spMetadataArrayHashSha256); + $spVersionIdNew = $result->fetchOne(); + + if ($spVersionIdNew !== false) { + return (int)$spVersionIdNew; + } + + $message = sprintf( + 'Error fetching SP Version even after insertion for SP ID %s.', + $spId + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving SP Version ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + protected function resolveUserId(HashDecoratedState $hashDecoratedState): int + { + $userIdentifierAttributeName = $this->moduleConfiguration->getUserIdAttributeName(); + + $userIdentifierValue = $hashDecoratedState->getState()->getAttributeValue($userIdentifierAttributeName); + if ($userIdentifierValue === null) { + $message = sprintf('Attributes do not contain user ID attribute %s.', $userIdentifierAttributeName); + throw new UnexpectedValueException($message); + } + + $userIdentifierValueHashSha256 = HashHelper::getSha256($userIdentifierValue); + + // Check if it already exists. + try { + $result = $this->repository->getUser($userIdentifierValueHashSha256); + $userId = $result->fetchOne(); + + if ($userId !== false) { + return (int)$userId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving user ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertUser($userIdentifierValue, $userIdentifierValueHashSha256); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new user, however, continuing in case of race condition. Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getUser($userIdentifierValueHashSha256); + $userIdNew = $result->fetchOne(); + + if ($userIdNew !== false) { + return (int)$userIdNew; + } + + $message = sprintf( + 'Error fetching user even after insertion for identifier value hash SHA256 %s.', + $userIdentifierValueHashSha256 + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving user ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + protected function resolveUserVersionId(int $userId, HashDecoratedState $hashDecoratedState): int + { + $attributeArrayHashSha256 = $hashDecoratedState->getAttributesArrayHashSha256(); + + // Check if it already exists. + try { + $result = $this->repository->getUserVersion($userId, $attributeArrayHashSha256); + $userVersionId = $result->fetchOne(); + + if ($userVersionId !== false) { + return (int)$userVersionId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving user version ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertUserVersion( + $userId, + serialize($hashDecoratedState->getState()->getAttributes()), + $attributeArrayHashSha256 + ); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new user version, however, continuing in case of race condition. Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getUserVersion($userId, $attributeArrayHashSha256); + $userVersionIdNew = $result->fetchOne(); + + if ($userVersionIdNew !== false) { + return (int)$userVersionIdNew; + } + + $message = sprintf( + 'Error fetching user version even after insertion for user ID %s.', + $userId + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving user version ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + protected function resolveIdpSpUserVersionId(int $idpVersionId, int $spVersionId, int $userVersionId): int + { + // Check if it already exists. + try { + $result = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId); + $IdpSpUserVersionId = $result->fetchOne(); + + if ($IdpSpUserVersionId !== false) { + return (int)$IdpSpUserVersionId; + } + } catch (\Throwable $exception) { + $message = sprintf('Error resolving IdpSpUserVersion ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + // Create new + try { + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error inserting new IdpSpUserVersion, however, continuing in case of race condition. ' . + 'Error was: %s.', + $exception->getMessage() + ); + $this->logger->warning($message); + } + + // Try again, this time it should exist... + try { + $result = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId); + $IdpSpUserVersionIdNew = $result->fetchOne(); + + if ($IdpSpUserVersionIdNew !== false) { + return (int)$IdpSpUserVersionIdNew; + } + + $message = sprintf( + 'Error fetching IdpSpUserVersion ID even after insertion for IdpVersion %s, SpVersion ID %s and ' . + 'UserVersion ID %s.', + $idpVersionId, + $spVersionId, + $userVersionId + ); + throw new StoreException($message); + } catch (\Throwable $exception) { + $message = sprintf('Error resolving IdpSpUserVersion ID. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getConnectedOrganizations(string $userIdentifierHashSha256): ConnectedServiceProvider\Bag + { + $connectedServiceProviderBag = new ConnectedServiceProvider\Bag(); + + $results = $this->repository->getConnectedServiceProviders($userIdentifierHashSha256); + + if (empty($results)) { + return $connectedServiceProviderBag; + } + + try { + $databasePlatform = $this->connection->dbal()->getDatabasePlatform(); + + /** @var array $result */ + foreach ($results as $result) { + $rawConnectedServiceProvider = new RawConnectedServiceProvider($result, $databasePlatform); + + $serviceProvider = new ServiceProvider($rawConnectedServiceProvider->getServiceProviderMetadata()); + $user = new User($rawConnectedServiceProvider->getUserAttributes()); + + $connectedServiceProviderBag->addOrReplace( + new ConnectedServiceProvider( + $serviceProvider, + $rawConnectedServiceProvider->getNumberOfAuthentications(), + $rawConnectedServiceProvider->getLastAuthenticationAt(), + $rawConnectedServiceProvider->getFirstAuthenticationAt(), + $user + ) + ); + } + } catch (\Throwable $exception) { + $message = sprintf( + 'Error populating connected service provider bag. Error was: %s', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + return $connectedServiceProviderBag; + + // TODO mivanci remove after unit tests +// $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder(); +// $lastMetadataAndAttributesQueryBuilder = $this->connection->dbal()->createQueryBuilder(); +// +// /** @psalm-suppress TooManyArguments */ +// $authenticationEventsQueryBuilder->select( +// 'vs.entity_id AS sp_entity_id', +// 'COUNT(vae.id) AS number_of_authentications', +// 'MAX(vae.happened_at) AS last_authentication_at', +// 'MIN(vae.happened_at) AS first_authentication_at', +// )->from('vds_authentication_event', 'vae') +// ->leftJoin( +// 'vae', +// 'vds_idp_sp_user_version', +// 'visuv', +// 'vae.idp_sp_user_version_id = visuv.id' +// ) +// ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id') +// ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id') +// ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id') +// ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id') +// ->where( +// 'vu.identifier_hash_sha256 = ' . +// $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256) +// ) +// ->groupBy('vs.id') +// ->orderBy('number_of_authentications', 'DESC'); +// +// /** @psalm-suppress TooManyArguments */ +// $lastMetadataAndAttributesQueryBuilder->select( +// 'vs.entity_id AS sp_entity_id', +// 'vsv.metadata AS sp_metadata', +// 'vuv.attributes AS user_attributes', +// // 'vsv.id AS sp_version_id', +// // 'vuv.id AS user_version_id', +// )->from('vds_authentication_event', 'vae') +// ->leftJoin( +// 'vae', +// 'vds_idp_sp_user_version', +// 'visuv', +// 'vae.idp_sp_user_version_id = visuv.id' +// ) +// ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id') +// ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id') +// ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id') +// ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id') +// ->leftJoin('vsv', 'vds_sp_version', 'vsv2', 'vsv.id = vsv2.id AND vsv.id < vsv2.id') +// ->leftJoin('vuv', 'vds_user_version', 'vuv2', 'vuv.id = vuv2.id AND vuv.id < vuv2.id') +// ->where( +// 'vu.identifier_hash_sha256 = ' . +// $lastMetadataAndAttributesQueryBuilder->createNamedParameter($userIdentifierHashSha256) +// ) +// ->andWhere('vsv2.id IS NULL') +// ->andWhere('vuv2.id IS NULL'); +// +// try { +// $numberOfAuthentications = +// $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociativeIndexed(); +// $lastMetadataAndAttributes = +// $lastMetadataAndAttributesQueryBuilder->executeQuery()->fetchAllAssociativeIndexed(); +// +// return array_merge_recursive($numberOfAuthentications, $lastMetadataAndAttributes); +// } catch (\Throwable $exception) { +// $message = sprintf( +// 'Error executing query to get connected organizations. Error was: %s.', +// $exception->getMessage() +// ); +// throw new StoreException($message, (int)$exception->getCode(), $exception); +// } + } + + + /** + * @throws StoreException + */ + public function getActivity(string $userIdentifierHashSha256): Activity\Bag + { + // TODO mivanci pagination + $results = $this->repository->getActivity($userIdentifierHashSha256); + + $activityBag = new Activity\Bag(); + + if (empty($results)) { + return $activityBag; + } + + try { + /** @var array $result */ + foreach ($results as $result) { + $rawActivity = new RawActivity($result, $this->connection->dbal()->getDatabasePlatform()); + $serviceProvider = new ServiceProvider($rawActivity->getServiceProviderMetadata()); + $user = new User($rawActivity->getUserAttributes()); + + $activityBag->add( + new Activity($serviceProvider, $user, $rawActivity->getHappenedAt()) + ); + } + } catch (\Throwable $exception) { + $message = sprintf( + 'Error populating activity bag. Error was: %s', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + return $activityBag; + + // TODO mivanci remove +// $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder(); +// +// /** @psalm-suppress TooManyArguments */ +// $authenticationEventsQueryBuilder->select( +// 'vae.happened_at', +// 'vsv.metadata AS sp_metadata', +// 'vuv.attributes AS user_attributes' +// )->from('vds_authentication_event', 'vae') +// ->leftJoin( +// 'vae', +// 'vds_idp_sp_user_version', +// 'visuv', +// 'vae.idp_sp_user_version_id = visuv.id' +// ) +// ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id') +// ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id') +// ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id') +// ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id') +// ->where( +// 'vu.identifier_hash_sha256 = ' . +// $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256) +// ) +// ->orderBy('vae.id', 'DESC'); +// +// try { +// $numberOfAuthentications = $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociative(); +// +// return array_merge_recursive($numberOfAuthentications); +// } catch (\Throwable $exception) { +// $message = sprintf( +// 'Error executing query to get connected organizations. Error was: %s.', +// $exception->getMessage() +// ); +// throw new StoreException($message, (int)$exception->getCode(), $exception); +// } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/HashDecoratedState.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/HashDecoratedState.php new file mode 100644 index 0000000000000000000000000000000000000000..f349682236936540bc54a559630753096c227eb4 --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/HashDecoratedState.php @@ -0,0 +1,76 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Helpers\HashHelper; + +class HashDecoratedState +{ + protected State $state; + protected string $identityProviderEntityIdHashSha256; + protected string $serviceProviderEntityIdHashSha256; + protected string $identityProviderMetadataArrayHashSha256; + protected string $serviceProviderMetadataArrayHashSha256; + protected string $attributesArrayHashSha256; + + public function __construct(State $state) + { + $this->state = $state; + + $this->identityProviderEntityIdHashSha256 = HashHelper::getSha256($state->getIdentityProviderEntityId()); + $this->identityProviderMetadataArrayHashSha256 = + HashHelper::getSha256ForArray($state->getIdentityProviderMetadata()); + + $this->serviceProviderEntityIdHashSha256 = HashHelper::getSha256($state->getServiceProviderEntityId()); + $this->serviceProviderMetadataArrayHashSha256 = + HashHelper::getSha256ForArray($state->getServiceProviderMetadata()); + + $this->attributesArrayHashSha256 = HashHelper::getSha256ForArray($state->getAttributes()); + } + + /** + * @return State + */ + public function getState(): State + { + return $this->state; + } + + /** + * @return string + */ + public function getIdentityProviderEntityIdHashSha256(): string + { + return $this->identityProviderEntityIdHashSha256; + } + + /** + * @return string + */ + public function getServiceProviderEntityIdHashSha256(): string + { + return $this->serviceProviderEntityIdHashSha256; + } + + /** + * @return string + */ + public function getIdentityProviderMetadataArrayHashSha256(): string + { + return $this->identityProviderMetadataArrayHashSha256; + } + + public function getServiceProviderMetadataArrayHashSha256(): string + { + return $this->serviceProviderMetadataArrayHashSha256; + } + + /** + * @return string + */ + public function getAttributesArrayHashSha256(): string + { + return $this->attributesArrayHashSha256; + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000000CreateIdpTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000000CreateIdpTable.php new file mode 100644 index 0000000000000000000000000000000000000000..66d8b08f2e4d861a0e5930cb5db719a08d58ec3e --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000000CreateIdpTable.php @@ -0,0 +1,69 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000000CreateIdpTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('idp'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('entity_id', Types::STRING) + ->setLength(TableConstants::COLUMN_ENTITY_ID_LENGTH); + + $table->addColumn('entity_id_hash_sha256', Types::STRING) + ->setLength(TableConstants::COLUMN_HASH_SHA265_HEXITS_LENGTH) + ->setFixed(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addUniqueConstraint(['entity_id_hash_sha256']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('idp'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000100CreateIdpVersionTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000100CreateIdpVersionTable.php new file mode 100644 index 0000000000000000000000000000000000000000..4c18cf2929b4ace28d84c16d3a81211904e7a383 --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000100CreateIdpVersionTable.php @@ -0,0 +1,74 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000100CreateIdpVersionTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('idp_version'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('idp_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('metadata', Types::TEXT); + + $table->addColumn('metadata_hash_sha256', Types::STRING) + ->setLength(TableConstants::COLUMN_HASH_SHA265_HEXITS_LENGTH) + ->setFixed(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addForeignKeyConstraint($this->preparePrefixedTableName('idp'), ['idp_id'], ['id']); + + $table->addUniqueConstraint(['metadata_hash_sha256']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('idp_version'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000200CreateSpTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000200CreateSpTable.php new file mode 100644 index 0000000000000000000000000000000000000000..b9418b6d1224d8597253b0f90c4e3ec06871bb3f --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000200CreateSpTable.php @@ -0,0 +1,69 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000200CreateSpTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('sp'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('entity_id', Types::STRING) + ->setLength(TableConstants::COLUMN_ENTITY_ID_LENGTH); + + $table->addColumn('entity_id_hash_sha256', Types::STRING) + ->setLength(TableConstants::COLUMN_HASH_SHA265_HEXITS_LENGTH) + ->setFixed(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addUniqueConstraint(['entity_id_hash_sha256']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('sp'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000300CreateSpVersionTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000300CreateSpVersionTable.php new file mode 100644 index 0000000000000000000000000000000000000000..13a55358e0491d17099973de3f98ab9385c44136 --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000300CreateSpVersionTable.php @@ -0,0 +1,74 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000300CreateSpVersionTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('sp_version'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('sp_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('metadata', Types::TEXT); + + $table->addColumn('metadata_hash_sha256', Types::STRING) + ->setLength(TableConstants::COLUMN_HASH_SHA265_HEXITS_LENGTH) + ->setFixed(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addForeignKeyConstraint($this->preparePrefixedTableName('sp'), ['sp_id'], ['id']); + + $table->addUniqueConstraint(['metadata_hash_sha256']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('sp_version'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000400CreateUserTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000400CreateUserTable.php new file mode 100644 index 0000000000000000000000000000000000000000..667d8ffa9e4d53a669005a49218d8b74ec9f98dd --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000400CreateUserTable.php @@ -0,0 +1,70 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000400CreateUserTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('user'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('identifier', Types::TEXT) + ->setLength(65535); + + $table->addColumn('identifier_hash_sha256', Types::STRING) + ->setLength(TableConstants::COLUMN_HASH_SHA265_HEXITS_LENGTH) + ->setFixed(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addUniqueConstraint(['identifier_hash_sha256']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('user'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000500CreateUserVersionTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000500CreateUserVersionTable.php new file mode 100644 index 0000000000000000000000000000000000000000..fcb2b42ca0e72b02d561eb395aad33c1154cfe7c --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000500CreateUserVersionTable.php @@ -0,0 +1,75 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000500CreateUserVersionTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('user_version'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('user_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('attributes', Types::TEXT) + ->setComment('Serialized attributes.'); + + $table->addColumn('attributes_hash_sha256', Types::STRING) + ->setLength(TableConstants::COLUMN_HASH_SHA265_HEXITS_LENGTH) + ->setFixed(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addForeignKeyConstraint($this->preparePrefixedTableName('user'), ['user_id'], ['id']); + + $table->addUniqueConstraint(['attributes_hash_sha256']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('user_version'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000600CreateIdpSpUserVersionTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000600CreateIdpSpUserVersionTable.php new file mode 100644 index 0000000000000000000000000000000000000000..625e6512f4c8f3f23d8c249e82d477e323054152 --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000600CreateIdpSpUserVersionTable.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000600CreateIdpSpUserVersionTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('idp_sp_user_version'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('idp_version_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('sp_version_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('user_version_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addForeignKeyConstraint( + $this->preparePrefixedTableName('idp_version'), + ['idp_version_id'], + ['id'] + ); + + $table->addForeignKeyConstraint( + $this->preparePrefixedTableName('sp_version'), + ['sp_version_id'], + ['id'] + ); + + $table->addForeignKeyConstraint( + $this->preparePrefixedTableName('user_version'), + ['user_version_id'], + ['id'] + ); + + $table->addUniqueConstraint(['idp_version_id', 'sp_version_id', 'user_version_id']); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('idp_sp_user_version'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php new file mode 100644 index 0000000000000000000000000000000000000000..d56082275448d94f189dfbb22b81f0b6404c3f9c --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php @@ -0,0 +1,72 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class Version20220801000700CreateAuthenticationEventTable extends AbstractMigration +{ + protected function getLocalTablePrefix(): string + { + return 'vds_'; + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function run(): void + { + $tableName = $this->preparePrefixedTableName('authentication_event'); + + try { + $table = new Table($tableName); + + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn('idp_sp_user_version_id', Types::BIGINT) + ->setUnsigned(true); + + $table->addColumn('happened_at', Types::DATETIMETZ_IMMUTABLE); + + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + + $table->addForeignKeyConstraint( + $this->preparePrefixedTableName('idp_sp_user_version'), + ['idp_sp_user_version_id'], + ['id'] + ); + + $this->schemaManager->createTable($table); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException( + \sprintf('Error creating table \'%s.', $tableName), + $exception + ); + } + } + + /** + * @inheritDoc + * @throws MigrationException + */ + public function revert(): void + { + $tableName = $this->preparePrefixedTableName('authentication_event'); + + try { + $this->schemaManager->dropTable($tableName); + } catch (\Throwable $exception) { + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivity.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivity.php new file mode 100644 index 0000000000000000000000000000000000000000..8a1f7f60ece229149941f7835aca5c343a354129 --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivity.php @@ -0,0 +1,126 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use DateTimeImmutable; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity; + +class RawActivity extends AbstractRawEntity +{ + protected array $serviceProviderMetadata; + protected array $userAttributes; + protected DateTimeImmutable $happenedAt; + + public function __construct(array $rawRow, AbstractPlatform $abstractPlatform) + { + parent::__construct($rawRow, $abstractPlatform); + + $this->serviceProviderMetadata = $this->resolveServiceProviderMetadata( + (string)$rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA] + ); + + $this->userAttributes = $this->resolveUserAttributes( + (string)$rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES] + ); + + $this->happenedAt = $this->resolveDateTimeImmutable( + $rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT] + ); + } + + /** + * @return DateTimeImmutable + */ + public function getHappenedAt(): DateTimeImmutable + { + return $this->happenedAt; + } + + /** + * @return array + */ + public function getServiceProviderMetadata(): array + { + return $this->serviceProviderMetadata; + } + + /** + * @return array + */ + public function getUserAttributes(): array + { + return $this->userAttributes; + } + + /** + * @inheritDoc + */ + protected function validate(array $rawRow): void + { + $columnsToCheck = [ + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA, + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES, + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT, + ]; + + foreach ($columnsToCheck as $column) { + if (empty($rawRow[$column])) { + throw new UnexpectedValueException(sprintf('Column %s must be set.', $column)); + } + } + + if (! is_string($rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA + ); + throw new UnexpectedValueException($message); + } + + if (! is_string($rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES + ); + throw new UnexpectedValueException($message); + } + + if (! is_string($rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT + ); + throw new UnexpectedValueException($message); + } + } + + protected function resolveServiceProviderMetadata(string $serializedMetadata): array + { + /** @psalm-suppress MixedAssignment - we check the type manually */ + $metadata = unserialize($serializedMetadata); + + if (is_array($metadata)) { + return $metadata; + } + + $message = sprintf('Metadata not in expected array format, got type %s.', gettype($metadata)); + throw new UnexpectedValueException($message); + } + + protected function resolveUserAttributes(string $serializedUserAttributes): array + { + /** @psalm-suppress MixedAssignment - we check the type manually */ + $userAttributes = unserialize($serializedUserAttributes); + + if (is_array($userAttributes)) { + return $userAttributes; + } + + $message = sprintf('User attributes not in expected array format, got type %s.', gettype($userAttributes)); + throw new UnexpectedValueException($message); + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawConnectedServiceProvider.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawConnectedServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..f9ed95a75e80ed431f60db63c2d3c8dba120af3c --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawConnectedServiceProvider.php @@ -0,0 +1,170 @@ +<?php + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use DateTimeImmutable; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity; + +class RawConnectedServiceProvider extends AbstractRawEntity +{ + protected int $numberOfAuthentications; + protected DateTimeImmutable $lastAuthenticationAt; + protected DateTimeImmutable $firstAuthenticationAt; + protected array $serviceProviderMetadata; + protected array $userAttributes; + + public function __construct(array $rawRow, AbstractPlatform $abstractPlatform) + { + parent::__construct($rawRow, $abstractPlatform); + + $this->numberOfAuthentications = (int)$rawRow[ + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS + ]; + + $this->lastAuthenticationAt = $this->resolveDateTimeImmutable( + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT] + ); + + $this->firstAuthenticationAt = $this->resolveDateTimeImmutable( + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT] + ); + + $this->serviceProviderMetadata = $this->resolveServiceProviderMetadata( + (string)$rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] + ); + + $this->userAttributes = $this->resolveUserAttributes( + (string)$rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] + ); + } + + /** + * @return int + */ + public function getNumberOfAuthentications(): int + { + return $this->numberOfAuthentications; + } + + /** + * @return DateTimeImmutable + */ + public function getLastAuthenticationAt(): DateTimeImmutable + { + return $this->lastAuthenticationAt; + } + + /** + * @return DateTimeImmutable + */ + public function getFirstAuthenticationAt(): DateTimeImmutable + { + return $this->firstAuthenticationAt; + } + + /** + * @return array + */ + public function getServiceProviderMetadata(): array + { + return $this->serviceProviderMetadata; + } + + /** + * @return array + */ + public function getUserAttributes(): array + { + return $this->userAttributes; + } + + /** + * @inheritDoc + */ + protected function validate(array $rawRow): void + { + $columnsToCheck = [ + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES, + ]; + + foreach ($columnsToCheck as $column) { + if (empty($rawRow[$column])) { + throw new UnexpectedValueException(sprintf('Column %s must be set.', $column)); + } + } + + if ( + ! is_numeric($rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS]) + ) { + $message = sprintf( + 'Column %s must be numeric.', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS + ); + throw new UnexpectedValueException($message); + } + + if (! is_string($rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT + ); + throw new UnexpectedValueException($message); + } + + if (! is_string($rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT + ); + throw new UnexpectedValueException($message); + } + + if (! is_string($rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA + ); + throw new UnexpectedValueException($message); + } + + if (! is_string($rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES])) { + $message = sprintf( + 'Column %s must be string.', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES + ); + throw new UnexpectedValueException($message); + } + } + + protected function resolveServiceProviderMetadata(string $serializedMetadata): array + { + /** @psalm-suppress MixedAssignment - we check the type manually */ + $metadata = unserialize($serializedMetadata); + + if (is_array($metadata)) { + return $metadata; + } + + $message = sprintf('Metadata not in expected array format, got type %s.', gettype($metadata)); + throw new UnexpectedValueException($message); + } + + protected function resolveUserAttributes(string $serializedUserAttributes): array + { + /** @psalm-suppress MixedAssignment - we check the type manually */ + $userAttributes = unserialize($serializedUserAttributes); + + if (is_array($userAttributes)) { + return $userAttributes; + } + + $message = sprintf('User attributes not in expected array format, got type %s.', gettype($userAttributes)); + throw new UnexpectedValueException($message); + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php new file mode 100644 index 0000000000000000000000000000000000000000..c3c41bd7aac393e9274ec46d05500366a16e728b --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php @@ -0,0 +1,1059 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Result; +use Doctrine\DBAL\Types\Types; +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use Throwable; + +class Repository +{ + protected Connection $connection; + protected LoggerInterface $logger; + protected string $tableNameIdp; + protected string $tableNameIdpVersion; + protected string $tableNameSp; + protected string $tableNameSpVersion; + protected string $tableNameUser; + protected string $tableNameUserVersion; + protected string $tableNameIdpSpUserVersion; + protected string $tableNameAuthenticationEvent; + + public function __construct(Connection $connection, LoggerInterface $logger) + { + $this->connection = $connection; + $this->logger = $logger; + + $this->tableNameIdp = $this->preparePrefixedTableName(TableConstants::TABLE_NAME_IDP); + $this->tableNameIdpVersion = $this->preparePrefixedTableName(TableConstants::TABLE_NAME_IDP_VERSION); + $this->tableNameSp = $this->preparePrefixedTableName(TableConstants::TABLE_NAME_SP); + $this->tableNameSpVersion = $this->preparePrefixedTableName(TableConstants::TABLE_NAME_SP_VERSION); + $this->tableNameUser = $this->preparePrefixedTableName(TableConstants::TABLE_NAME_USER); + $this->tableNameUserVersion = $this->preparePrefixedTableName(TableConstants::TABLE_NAME_USER_VERSION); + $this->tableNameIdpSpUserVersion = + $this->preparePrefixedTableName(TableConstants::TABLE_NAME_IDP_SP_USER_VERSION); + $this->tableNameAuthenticationEvent = + $this->preparePrefixedTableName(TableConstants::TABLE_NAME_AUTHENTICATION_EVENT); + } + + protected function preparePrefixedTableName(string $tableName): string + { + return $this->connection->preparePrefixedTableName(TableConstants::TABLE_PREFIX . $tableName); + } + + /** + * @throws StoreException + */ + public function getIdp(string $entityIdHashSha256): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_IDP_COLUMN_NAME_ID, + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID, + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256, + TableConstants::TABLE_IDP_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameIdp) + ->where( + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 . ' = ' . + $queryBuilder->createNamedParameter($entityIdHashSha256) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get IdP by entity ID hash SHA256 \'%s\'. Error was: %s.', + $entityIdHashSha256, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertIdp( + string $entityId, + string $entityIdHashSha256, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameIdp) + ->values( + [ + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID => ':' . + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID, + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 => ':' . + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256, + TableConstants::TABLE_IDP_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_IDP_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID => $entityId, + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 => $entityIdHashSha256, + TableConstants::TABLE_IDP_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID => Types::STRING, + TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 => Types::STRING, + TableConstants::TABLE_IDP_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Error executing query to insert IdP. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getIdpVersion(int $idpId, string $metadataHashSha256): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_ID, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameIdpVersion) + ->where( + $queryBuilder->expr()->and( + $queryBuilder->expr()->eq( + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID, + $queryBuilder->createNamedParameter($idpId, ParameterType::INTEGER) + ), + $queryBuilder->expr()->eq( + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256, + $queryBuilder->createNamedParameter($metadataHashSha256) + ) + ) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get IdP Version for IdP %s and metadata array hash %s. Error was: %s.', + $idpId, + $metadataHashSha256, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertIdpVersion( + int $idpId, + string $metadata, + string $metadataHashSha256, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameIdpVersion) + ->values( + [ + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID => ':' . + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA => ':' . + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 => ':' . + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID => $idpId, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA => $metadata, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 => $metadataHashSha256, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID => Types::BIGINT, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA => Types::TEXT, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 => Types::STRING, + TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE, + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Error executing query to insert IdP Version. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getSp(string $entityIdHashSha256): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_SP_COLUMN_NAME_ID, + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID, + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256, + TableConstants::TABLE_SP_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameSp) + ->where( + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 . ' = ' . + $queryBuilder->createNamedParameter($entityIdHashSha256) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get SP by entity ID hash SHA256 \'%s\'. Error was: %s.', + $entityIdHashSha256, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + public function insertSp( + string $entityId, + string $entityIdHashSha256, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameSp) + ->values( + [ + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID => ':' . + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID, + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 => ':' . + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256, + TableConstants::TABLE_SP_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_SP_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID => $entityId, + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 => $entityIdHashSha256, + TableConstants::TABLE_SP_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID => Types::STRING, + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 => Types::STRING, + TableConstants::TABLE_SP_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Error executing query to insert SP. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getSpVersion(int $spId, string $metadataHashSha256): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameSpVersion) + ->where( + $queryBuilder->expr()->and( + $queryBuilder->expr()->eq( + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID, + $queryBuilder->createNamedParameter($spId, ParameterType::INTEGER) + ), + $queryBuilder->expr()->eq( + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256, + $queryBuilder->createNamedParameter($metadataHashSha256) + ) + ) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get SP Version for SP %s and metadata array hash %s. Error was: %s.', + $spId, + $metadataHashSha256, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertSpVersion( + int $spId, + string $metadata, + string $metadataHashSha256, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameSpVersion) + ->values( + [ + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID => ':' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA => ':' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 => ':' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID => $spId, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA => $metadata, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 => $metadataHashSha256, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID => Types::BIGINT, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA => Types::TEXT, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 => Types::STRING, + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE, + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Error executing query to insert SP Version. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getUser(string $identifierHashSha256): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_USER_COLUMN_NAME_ID, + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER, + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256, + TableConstants::TABLE_USER_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameUser) + ->where( + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 . ' = ' . + $queryBuilder->createNamedParameter($identifierHashSha256) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get user by identifier hash SHA256 \'%s\'. Error was: %s.', + $identifierHashSha256, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertUser( + string $identifier, + string $identifierHashSha256, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameUser) + ->values( + [ + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER => ':' . + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER, + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 => ':' . + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256, + TableConstants::TABLE_USER_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_USER_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER => $identifier, + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 => $identifierHashSha256, + TableConstants::TABLE_USER_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER => Types::TEXT, + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 => Types::STRING, + TableConstants::TABLE_USER_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Error executing query to insert user. Error was: %s.', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getUserVersion(int $userId, string $attributesHashSha256): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameUserVersion) + ->where( + $queryBuilder->expr()->and( + $queryBuilder->expr()->eq( + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID, + $queryBuilder->createNamedParameter($userId, ParameterType::INTEGER) + ), + $queryBuilder->expr()->eq( + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256, + $queryBuilder->createNamedParameter($attributesHashSha256) + ) + ) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get user version for user ID %s and attribute array hash %s. Error was: %s.', + $userId, + $attributesHashSha256, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertUserVersion( + int $userId, + string $attributes, + string $attributesHashSha256, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameUserVersion) + ->values( + [ + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID => ':' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES => ':' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256 => ':' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID => $userId, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES => $attributes, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256 => $attributesHashSha256, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID => Types::BIGINT, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES => Types::TEXT, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256 => Types::STRING, + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE, + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to insert user version. Error was: %s.', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getIdpSpUserVersion(int $idpVersionId, int $spVersionId, int $userVersionId): Result + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $queryBuilder->select( + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT, + ) + ->from($this->tableNameIdpSpUserVersion) + ->where( + $queryBuilder->expr()->and( + $queryBuilder->expr()->eq( + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID, + $queryBuilder->createNamedParameter($idpVersionId, ParameterType::INTEGER) + ), + $queryBuilder->expr()->eq( + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID, + $queryBuilder->createNamedParameter($spVersionId, ParameterType::INTEGER) + ), + $queryBuilder->expr()->eq( + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID, + $queryBuilder->createNamedParameter($userVersionId, ParameterType::INTEGER) + ) + ) + )->setMaxResults(1); + + return $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to get IdpSpUserVersion for IdpVersion %s, SpVersion %s and UserVersion %s.' . + ' Error was: %s.', + $idpVersionId, + $spVersionId, + $userVersionId, + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertIdpSpUserVersion( + int $idpVersionId, + int $spVersionId, + int $userVersionId, + \DateTimeImmutable $createdAt = null + ): void { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameIdpSpUserVersion) + ->values( + [ + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID => ':' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID => ':' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID => ':' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID => $idpVersionId, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID => $spVersionId, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID => $userVersionId, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID => Types::BIGINT, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID => Types::BIGINT, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID => Types::BIGINT, + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to insert IdpSpUserVersion. Error was: %s.', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function insertAuthenticationEvent( + int $IdpSpUserVersionId, + \DateTimeImmutable $happenedAt, + \DateTimeImmutable $createdAt = null + ): void { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $createdAt = $createdAt ?? new \DateTimeImmutable(); + + $queryBuilder->insert($this->tableNameAuthenticationEvent) + ->values( + [ + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID => ':' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID, + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT => ':' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT, + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_CREATED_AT => ':' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID => + $IdpSpUserVersionId, + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT => $happenedAt, + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_CREATED_AT => $createdAt, + ], + [ + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID => + Types::BIGINT, + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT => + Types::DATETIMETZ_IMMUTABLE, + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_CREATED_AT => + Types::DATETIMETZ_IMMUTABLE, + ] + ); + + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to insert AuthenticationEvent. Error was: %s.', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getConnectedServiceProviders(string $userIdentifierHashSha256): array + { + try { + $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $lastMetadataAndAttributesQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $authenticationEventsQueryBuilder->select( + //'vs.entity_id AS sp_entity_id', + TableConstants::TABLE_ALIAS_SP . '.' . + TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID . ' AS ' . + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_ENTITY_ID, + //'COUNT(vae.id) AS number_of_authentications', + 'COUNT(' . TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_ID . ') AS ' . + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS, + //'MAX(vae.happened_at) AS last_authentication_at', + 'MAX(' . TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT . ') AS ' . + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT, + //'MIN(vae.happened_at) AS first_authentication_at', + 'MIN(' . TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT . ') AS ' . + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT, + )->from($this->tableNameAuthenticationEvent, TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT) + ->leftJoin( + //'vae', + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT, + //'vds_idp_sp_user_version', + $this->tableNameIdpSpUserVersion, + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vae.idp_sp_user_version_id = visuv.id' + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vds_sp_version', + $this->tableNameSpVersion, + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'visuv.sp_version_id = vsv.id' + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'vds_sp', + $this->tableNameSp, + //'vs', + TableConstants::TABLE_ALIAS_SP, + //'vsv.sp_id = vs.id' + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID . ' = ' . + TableConstants::TABLE_ALIAS_SP . '.' . TableConstants::TABLE_SP_COLUMN_NAME_ID + ) + ->leftJoin( + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vds_user_version', + $this->tableNameUserVersion, + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'visuv.user_version_id = vuv.id' + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'vds_user', + $this->tableNameUser, + //'vu', + TableConstants::TABLE_ALIAS_USER, + //'vuv.user_id = vu.id' + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID . ' = ' . + TableConstants::TABLE_ALIAS_USER . '.' . TableConstants::TABLE_USER_COLUMN_NAME_ID + ) + ->where( + //'vu.identifier_hash_sha256 = ' . + TableConstants::TABLE_ALIAS_USER . '.' . + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 . ' = ' . + $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256) + ) + ->groupBy( + //'vs.id' + TableConstants::TABLE_ALIAS_SP . '.' . TableConstants::TABLE_SP_COLUMN_NAME_ID + ) + ->orderBy( + //'number_of_authentications', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS, + 'DESC' + ); + + + /** @psalm-suppress TooManyArguments */ + $lastMetadataAndAttributesQueryBuilder->select( + //'vs.entity_id AS sp_entity_id', + TableConstants::TABLE_ALIAS_SP . '.' . TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID . ' AS ' . + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_ENTITY_ID, + //'vsv.metadata AS sp_metadata', + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA . + ' AS ' . TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA, + //'vuv.attributes AS user_attributes', + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES . ' AS ' . + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES + // 'vsv.id AS sp_version_id', + // 'vuv.id AS user_version_id', + )->from( + //'vds_authentication_event', + $this->tableNameAuthenticationEvent, + //'vae' + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT + ) + ->leftJoin( + //'vae', + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT, + //'vds_idp_sp_user_version', + $this->tableNameIdpSpUserVersion, + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vae.idp_sp_user_version_id = visuv.id' + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vds_sp_version', + $this->tableNameSpVersion, + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'visuv.sp_version_id = vsv.id' + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'vds_sp', + $this->tableNameSp, + //'vs', + TableConstants::TABLE_ALIAS_SP, + //'vsv.sp_id = vs.id' + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID . ' = ' . TableConstants::TABLE_ALIAS_SP . '.' . + TableConstants::TABLE_SP_COLUMN_NAME_ID + ) + ->leftJoin( + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vds_user_version', + $this->tableNameUserVersion, + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'visuv.user_version_id = vuv.id' + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'vds_user', + $this->tableNameUser, + //'vu', + TableConstants::TABLE_ALIAS_USER, + //'vuv.user_id = vu.id' + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID . ' = ' . TableConstants::TABLE_ALIAS_USER . + '.' . TableConstants::TABLE_USER_COLUMN_NAME_ID + ) + ->leftJoin( + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'vds_sp_version', + $this->tableNameSpVersion, + //'vsv2', + TableConstants::TABLE_ALIAS_SP_VERSION_2, // Another alias for self joining... + //'vsv.id = vsv2.id AND vsv.id < vsv2.id' // To be able to get latest one... + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID . ' = ' . TableConstants::TABLE_ALIAS_SP_VERSION_2 . + '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID . ' AND ' . + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID . + ' < ' . TableConstants::TABLE_ALIAS_SP_VERSION_2 . '.' . + TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'vds_user_version', + $this->tableNameUserVersion, + //'vuv2', + TableConstants::TABLE_ALIAS_USER_VERSION_2, // Another alias for self joining... + //'vuv.id = vuv2.id AND vuv.id < vuv2.id' // To be able to get latest one... + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID . ' = ' . + TableConstants::TABLE_ALIAS_USER_VERSION_2 + . '.' . TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID . ' AND ' . + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID . + ' < ' . TableConstants::TABLE_ALIAS_USER_VERSION_2 . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID + ) + ->where( + //'vu.identifier_hash_sha256 = ' . + TableConstants::TABLE_ALIAS_USER . '.' . + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 . ' = ' . + $lastMetadataAndAttributesQueryBuilder->createNamedParameter($userIdentifierHashSha256) + ) + ->andWhere( + //'vsv2.id IS NULL' + TableConstants::TABLE_ALIAS_SP_VERSION_2 . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID + . ' IS NULL' + ) + ->andWhere( + //'vuv2.id IS NULL' + TableConstants::TABLE_ALIAS_USER_VERSION_2 . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID . ' IS NULL' + ); + + $numberOfAuthentications = $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociativeIndexed(); + $lastMetadataAndAttributes = + $lastMetadataAndAttributesQueryBuilder->executeQuery()->fetchAllAssociativeIndexed(); + + return array_merge_recursive($numberOfAuthentications, $lastMetadataAndAttributes); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error executing query to get connected organizations. Error was: %s.', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + public function getActivity(string $userIdentifierHashSha256): array + { + try { + $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** @psalm-suppress TooManyArguments */ + $authenticationEventsQueryBuilder->select( + //'vae.happened_at', + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT, + //'vsv.metadata AS sp_metadata', + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA . + ' AS ' . TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA, + //'vuv.attributes AS user_attributes' + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES . ' AS ' . + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES + )->from( + //'vds_authentication_event', 'vae' + $this->tableNameAuthenticationEvent, + //'vae' + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT + ) + ->leftJoin( + //'vae', + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT, + //'vds_idp_sp_user_version', + $this->tableNameIdpSpUserVersion, + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vae.idp_sp_user_version_id = visuv.id' + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vds_sp_version', + $this->tableNameSpVersion, + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'visuv.sp_version_id = vsv.id' + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vsv', + TableConstants::TABLE_ALIAS_SP_VERSION, + //'vds_sp', + $this->tableNameSp, + //'vs', + TableConstants::TABLE_ALIAS_SP, + //'vsv.sp_id = vs.id' + TableConstants::TABLE_ALIAS_SP_VERSION . '.' . TableConstants::TABLE_SP_VERSION_COLUMN_NAME_SP_ID . + ' = ' . TableConstants::TABLE_ALIAS_SP . '.' . TableConstants::TABLE_SP_COLUMN_NAME_ID + ) + ->leftJoin( + //'visuv', + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION, + //'vds_user_version', + $this->tableNameUserVersion, + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'visuv.user_version_id = vuv.id' + TableConstants::TABLE_ALIAS_IDP_SP_USER_VERSION . '.' . + TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID . ' = ' . + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID + ) + ->leftJoin( + //'vuv', + TableConstants::TABLE_ALIAS_USER_VERSION, + //'vds_user', + $this->tableNameUser, + //'vu', + TableConstants::TABLE_ALIAS_USER, + //'vuv.user_id = vu.id' + TableConstants::TABLE_ALIAS_USER_VERSION . '.' . + TableConstants::TABLE_USER_VERSION_COLUMN_NAME_USER_ID . ' = ' . TableConstants::TABLE_ALIAS_USER . + '.' . TableConstants::TABLE_USER_COLUMN_NAME_ID + ) + ->where( + //'vu.identifier_hash_sha256 = ' . + TableConstants::TABLE_ALIAS_USER . '.' . + TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 . ' = ' . + $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256) + ) + ->orderBy( + //'vae.id', + TableConstants::TABLE_ALIAS_AUTHENTICATION_EVENT . '.' . + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_ID, + 'DESC' + ); + + return $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $exception) { + $message = sprintf( + 'Error executing query to get connected organizations. Error was: %s.', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } +} diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/TableConstants.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/TableConstants.php new file mode 100644 index 0000000000000000000000000000000000000000..30aa9a5a76a8b6114df8a7a97915d716604ee3ce --- /dev/null +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/TableConstants.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +class TableConstants +{ + public const TABLE_PREFIX = 'vds_'; // versioned data store + + // Any SAML entity ID should have maximum 1024 chars per + // https://stackoverflow.com/questions/24196369/what-to-present-at-saml-entityid-url + public const COLUMN_ENTITY_ID_LENGTH = 1024; + public const COLUMN_HASH_SHA265_HEXITS_LENGTH = 64; + + + // Table 'idp' + public const TABLE_NAME_IDP = 'idp'; + public const TABLE_ALIAS_IDP = self::TABLE_PREFIX . 'i'; + public const TABLE_IDP_COLUMN_NAME_ID = 'id'; // int + public const TABLE_IDP_COLUMN_NAME_ENTITY_ID = 'entity_id'; // Entity ID value, string, varchar(1024) + public const TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 = 'entity_id_hash_sha256'; // ha256 hash hexits, char(64) + public const TABLE_IDP_COLUMN_NAME_CREATED_AT = 'created_at'; // First time IdP usage, datetime + + // Table 'idp_version' + public const TABLE_NAME_IDP_VERSION = 'idp_version'; + public const TABLE_ALIAS_IDP_VERSION = self::TABLE_PREFIX . 'iv'; + public const TABLE_IDP_VERSION_COLUMN_NAME_ID = 'id'; // int ID + public const TABLE_IDP_VERSION_COLUMN_NAME_IDP_ID = 'idp_id'; // FK + public const TABLE_IDP_VERSION_COLUMN_NAME_METADATA = 'metadata'; // Serialized IdP metadata version + public const TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 = 'metadata_hash_sha256'; // Metadata sha256 hash + public const TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Table 'sp', same structure as in 'idp' + public const TABLE_NAME_SP = 'sp'; + public const TABLE_ALIAS_SP = self::TABLE_PREFIX . 's'; + public const TABLE_SP_COLUMN_NAME_ID = 'id'; + public const TABLE_SP_COLUMN_NAME_ENTITY_ID = 'entity_id'; + public const TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256 = 'entity_id_hash_sha256'; + public const TABLE_SP_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Table 'sp_version', same structure as in 'idp_version' + public const TABLE_NAME_SP_VERSION = 'sp_version'; + public const TABLE_ALIAS_SP_VERSION = self::TABLE_PREFIX . 'sv'; + public const TABLE_ALIAS_SP_VERSION_2 = self::TABLE_ALIAS_SP_VERSION . '_2'; + public const TABLE_SP_VERSION_COLUMN_NAME_ID = 'id'; + public const TABLE_SP_VERSION_COLUMN_NAME_SP_ID = 'sp_id'; + public const TABLE_SP_VERSION_COLUMN_NAME_METADATA = 'metadata'; + public const TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256 = 'metadata_hash_sha256'; + public const TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Table 'user' + public const TABLE_NAME_USER = 'user'; + public const TABLE_ALIAS_USER = self::TABLE_PREFIX . 'u'; + public const TABLE_USER_COLUMN_NAME_ID = 'id'; // int + public const TABLE_USER_COLUMN_NAME_IDENTIFIER = 'identifier'; // text, varies... (can be ePTID, which is long XML). + public const TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256 = 'identifier_hash_sha256'; + public const TABLE_USER_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Table 'user_version' (versioned attributes) + public const TABLE_NAME_USER_VERSION = 'user_version'; + public const TABLE_ALIAS_USER_VERSION = self::TABLE_PREFIX . 'uv'; + public const TABLE_ALIAS_USER_VERSION_2 = self::TABLE_ALIAS_USER_VERSION . '_2'; + public const TABLE_USER_VERSION_COLUMN_NAME_ID = 'id'; // int ID + public const TABLE_USER_VERSION_COLUMN_NAME_USER_ID = 'user_id'; // FK + public const TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES = 'attributes'; // Serialized attributes version + public const TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256 = 'attributes_hash_sha256'; + public const TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Attribute versions released to SP version + public const TABLE_NAME_IDP_SP_USER_VERSION = 'idp_sp_user_version'; + public const TABLE_ALIAS_IDP_SP_USER_VERSION = self::TABLE_PREFIX . 'isuv'; + public const TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID = 'id'; + public const TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID = 'idp_version_id'; + public const TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID = 'sp_version_id'; + public const TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_USER_VERSION_ID = 'user_version_id'; + public const TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Table 'authentication_event'. + public const TABLE_NAME_AUTHENTICATION_EVENT = 'authentication_event'; + public const TABLE_ALIAS_AUTHENTICATION_EVENT = self::TABLE_PREFIX . 'ae'; + public const TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_ID = 'id'; + public const TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_IDP_SP_USER_VERSION_ID = 'idp_sp_user_version_id'; + public const TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT = 'happened_at'; + public const TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_CREATED_AT = 'created_at'; + + // Entity 'ConnectedOrganization' (service provider) related. + public const ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_ENTITY_ID = 'sp_entity_id'; + public const ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS = 'number_of_authentications'; + public const ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT = 'last_authentication_at'; + public const ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT = 'first_authentication_at'; + public const ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA = 'sp_metadata'; + public const ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES = 'user_attributes'; + + // Entity 'Activity' related. + public const ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA = 'sp_metadata'; + public const ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES = 'user_attributes'; + public const ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT = 'happened_at'; +} diff --git a/src/Stores/Interfaces/DataStoreInterface.php b/src/Stores/Interfaces/DataStoreInterface.php index 4d06925cd631780d351bc7eca618522d7829c726..81bcdcebf670d4079eb5707ee6c1ce00bc0fc297 100644 --- a/src/Stores/Interfaces/DataStoreInterface.php +++ b/src/Stores/Interfaces/DataStoreInterface.php @@ -4,6 +4,23 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Interfaces; +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Entities\Activity; +use SimpleSAML\Module\accounting\ModuleConfiguration; + interface DataStoreInterface extends StoreInterface { + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self; + + public function persist(Event $authenticationEvent): void; + + public function getConnectedOrganizations(string $userIdentifierHashSha256): ConnectedServiceProvider\Bag; + + public function getActivity(string $userIdentifierHashSha256): Activity\Bag; } diff --git a/src/Stores/Interfaces/JobsStoreInterface.php b/src/Stores/Interfaces/JobsStoreInterface.php index 0ea21d761bcea526fd3e73859b28e1725284cdd3..1ff238d66b921fc0005587f66fdb17972475b1b7 100644 --- a/src/Stores/Interfaces/JobsStoreInterface.php +++ b/src/Stores/Interfaces/JobsStoreInterface.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Interfaces; +use Psr\Log\LoggerInterface; use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; interface JobsStoreInterface extends StoreInterface { @@ -21,4 +23,10 @@ interface JobsStoreInterface extends StoreInterface * @return ?JobInterface */ public function dequeue(string $type = null): ?JobInterface; + + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self; } diff --git a/src/Stores/Interfaces/StoreInterface.php b/src/Stores/Interfaces/StoreInterface.php index ea80efa40131b993caac696176123e5a7d1544a4..29b1bd0b9734eef04185e3781e4c67b08702e94b 100644 --- a/src/Stores/Interfaces/StoreInterface.php +++ b/src/Stores/Interfaces/StoreInterface.php @@ -4,7 +4,16 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Interfaces; -interface StoreInterface +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\Interfaces\SetupableInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +interface StoreInterface extends BuildableUsingModuleConfigurationInterface, SetupableInterface { - public function needsSetUp(): bool; + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self; } diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php deleted file mode 100644 index d807e673e5040ff7b150f7d0d6ed782c4f49f3bf..0000000000000000000000000000000000000000 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; - -use Doctrine\DBAL\Schema\Table; -use Doctrine\DBAL\Types\Types; -use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; -use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; -use Throwable; - -class Version20220601000000CreateJobsTable extends AbstractMigration -{ - /** - * @throws MigrationException - */ - public function run(): void - { - $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); - - try { - $table = new Table($tableName); - - $table->addColumn(JobsStore::COLUMN_NAME_ID, Types::BIGINT) - ->setUnsigned(true) - ->setAutoincrement(true); - - $table->addColumn(JobsStore::COLUMN_NAME_TYPE, Types::STRING) - ->setLength(JobsStore::COLUMN_LENGTH_TYPE); - - $table->addColumn(JobsStore::COLUMN_NAME_PAYLOAD, Types::TEXT); - $table->addColumn(JobsStore::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE); - - $table->setPrimaryKey([JobsStore::COLUMN_NAME_ID]); - - $this->schemaManager->createTable($table); - } catch (Throwable $exception) { - $contextDetails = sprintf('Could not create table %s.', $tableName); - $this->throwGenericMigrationException($contextDetails, $exception); - } - } - - /** - * @throws MigrationException - */ - public function revert(): void - { - $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); - - try { - $this->schemaManager->dropTable($tableName); - } catch (Throwable $exception) { - $contextDetails = sprintf('Could not drop table %s.', $tableName); - $this->throwGenericMigrationException($contextDetails, $exception); - } - } -} diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/RawJob.php b/src/Stores/Jobs/DoctrineDbal/JobsStore/RawJob.php deleted file mode 100644 index f22a73f66182fd9d20df945ac527a9c01104d145..0000000000000000000000000000000000000000 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore/RawJob.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; - -use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Types\Type; -use Doctrine\DBAL\Types\Types; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; -use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; - -class RawJob -{ - protected int $id; - protected AbstractPayload $payload; - protected string $type; - protected \DateTimeImmutable $createdAt; - protected AbstractPlatform $abstractPlatform; - - public function __construct(array $rawRow, AbstractPlatform $abstractPlatform) - { - $this->abstractPlatform = $abstractPlatform; - - $this->validate($rawRow); - - $this->id = (int)$rawRow[JobsStore::COLUMN_NAME_ID]; - $this->payload = $this->resolvePayload((string)$rawRow[JobsStore::COLUMN_NAME_PAYLOAD]); - $this->type = (string)$rawRow[JobsStore::COLUMN_NAME_TYPE]; - $this->createdAt = $this->resolveCreatedAt($rawRow); - } - - protected function validate(array $rawRow): void - { - $columnsToCheck = [ - JobsStore::COLUMN_NAME_ID, - JobsStore::COLUMN_NAME_PAYLOAD, - JobsStore::COLUMN_NAME_TYPE, - JobsStore::COLUMN_NAME_CREATED_AT, - ]; - - foreach ($columnsToCheck as $column) { - if (empty($rawRow[$column])) { - throw new UnexpectedValueException(sprintf('Column %s must be set.', $column)); - } - } - - if (! is_numeric($rawRow[JobsStore::COLUMN_NAME_ID])) { - throw new UnexpectedValueException(sprintf('Column %s must be numeric.', JobsStore::COLUMN_NAME_ID)); - } - - if (! is_string($rawRow[JobsStore::COLUMN_NAME_PAYLOAD])) { - throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_PAYLOAD)); - } - - if (! is_string($rawRow[JobsStore::COLUMN_NAME_TYPE])) { - throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_TYPE)); - } - - if (! is_string($rawRow[JobsStore::COLUMN_NAME_CREATED_AT])) { - throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_CREATED_AT)); - } - } - - protected function resolvePayload(string $rawPayload): AbstractPayload - { - /** @psalm-suppress MixedAssignment - we check the type manually */ - $payload = unserialize($rawPayload); - - if ($payload instanceof AbstractPayload) { - return $payload; - } - - throw new UnexpectedValueException('Job payload is not instance of AbstractPayload.'); - } - - /** - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * @return AbstractPayload - */ - public function getPayload(): AbstractPayload - { - return $this->payload; - } - - /** - * @return string - */ - public function getType(): string - { - return $this->type; - } - - protected function resolveCreatedAt(array $rawRow): \DateTimeImmutable - { - try { - /** @var \DateTimeImmutable $createdAt */ - $createdAt = (Type::getType(Types::DATETIME_IMMUTABLE)) - ->convertToPHPValue($rawRow[JobsStore::COLUMN_NAME_CREATED_AT], $this->abstractPlatform); - } catch (\Throwable $exception) { - throw new UnexpectedValueException( - sprintf( - 'Could not create instance of DateTimeImmutable using value %s for column %s.', - var_export($rawRow[JobsStore::COLUMN_NAME_CREATED_AT], true), - JobsStore::COLUMN_NAME_CREATED_AT - ), - (int)$exception->getCode(), - $exception - ); - } - - return $createdAt; - } - - /** - * @return \DateTimeImmutable - */ - public function getCreatedAt(): \DateTimeImmutable - { - return $this->createdAt; - } -} diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore.php b/src/Stores/Jobs/DoctrineDbal/Store.php similarity index 52% rename from src/Stores/Jobs/DoctrineDbal/JobsStore.php rename to src/Stores/Jobs/DoctrineDbal/Store.php index 5507ab339391a7b488eb5328f6303c01f9884e43..5e81988ffc71ab00496969bf0316c1a2ecad1527 100644 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore.php +++ b/src/Stores/Jobs/DoctrineDbal/Store.php @@ -5,53 +5,38 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal; use Psr\Log\LoggerInterface; -use ReflectionClass; use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; use SimpleSAML\Module\accounting\Exceptions\StoreException; -use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; -use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; -use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; use SimpleSAML\Module\accounting\Stores\Interfaces\JobsStoreInterface; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Repository; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\TableConstants; use Throwable; -class JobsStore implements JobsStoreInterface +class Store extends AbstractStore implements JobsStoreInterface { - public const TABLE_NAME_JOBS = 'jobs'; - public const TABLE_NAME_FAILED_JOBS = 'failed_jobs'; - - public const COLUMN_NAME_ID = 'id'; - public const COLUMN_NAME_PAYLOAD = 'payload'; - public const COLUMN_NAME_TYPE = 'type'; - public const COLUMN_NAME_CREATED_AT = 'created_at'; - - public const COLUMN_LENGTH_TYPE = 1024; - - protected ModuleConfiguration $moduleConfiguration; - protected Connection $connection; protected string $prefixedTableNameJobs; protected string $prefixedTableNameFailedJobs; - protected Migrator $migrator; - protected LoggerInterface $logger; protected Repository $jobsRepository; + /** + * @throws StoreException + */ public function __construct( ModuleConfiguration $moduleConfiguration, - Factory $factory, LoggerInterface $logger, + Factory $connectionFactory, + string $connectionKey = null, Repository $jobsRepository = null ) { - $this->moduleConfiguration = $moduleConfiguration; - $this->connection = $factory->buildConnection($moduleConfiguration->getStoreConnection(self::class)); - $this->migrator = $factory->buildMigrator($this->connection); - $this->logger = $logger; + parent::__construct($moduleConfiguration, $logger, $connectionFactory, $connectionKey); - $this->prefixedTableNameJobs = $this->connection->preparePrefixedTableName(self::TABLE_NAME_JOBS); - $this->prefixedTableNameFailedJobs = $this->connection->preparePrefixedTableName(self::TABLE_NAME_FAILED_JOBS); + $this->prefixedTableNameJobs = $this->connection->preparePrefixedTableName(TableConstants::TABLE_NAME_JOB); + $this->prefixedTableNameFailedJobs = $this->connection + ->preparePrefixedTableName(TableConstants::TABLE_NAME_JOB_FAILED); $this->jobsRepository = $jobsRepository ?? new Repository($this->connection, $this->prefixedTableNameJobs, $this->logger); @@ -74,7 +59,10 @@ class JobsStore implements JobsStoreInterface $job = null; $attempts = 0; $maxDeleteAttempts = 3; + $this->connection->dbal()->getTransactionIsolation(); + // Do the dequeue without using transactions, since the underlying database engine might not support it + // (for example, MyISAM engine in MySQL database). try { // Check if there are any jobs in the store... while (($job = $this->jobsRepository->getNext($type)) !== null) { @@ -92,8 +80,12 @@ class JobsStore implements JobsStoreInterface // It seems that this job has already been deleted in the meantime. // Check if this happened before. If threshold is reached, throw. // Otherwise, try to get next job again. + $message = sprintf( + 'Job retrieval was successful, however it was deleted in the meantime. Attempt: %s', + $attempts + ); + $this->logger->warning($message, ['jobId' => $jobId]); if ($attempts > $maxDeleteAttempts) { - $message = 'Job retrieval was successful, however it was deleted in the meantime.'; throw new StoreException($message); } @@ -114,62 +106,6 @@ class JobsStore implements JobsStoreInterface return $job; } - /** - * @throws StoreException - * @throws MigrationException - */ - public function runSetup(): void - { - if ($this->migrator->needsSetup()) { - $this->migrator->runSetup(); - } - - if (!$this->areAllMigrationsImplemented()) { - $this->migrator->runNonImplementedMigrationClasses( - $this->getMigrationsDirectory(), - $this->getMigrationsNamespace() - ); - } - } - - /** - * @throws StoreException - */ - public function needsSetup(): bool - { - // ... if the migrator itself needs setup. - if ($this->migrator->needsSetup()) { - return true; - } - - // ... if JobsStore migrations need to run - if (!$this->areAllMigrationsImplemented()) { - return true; - } - - return false; - } - - public function areAllMigrationsImplemented(): bool - { - return !$this->migrator->hasNonImplementedMigrationClasses( - $this->getMigrationsDirectory(), - $this->getMigrationsNamespace() - ); - } - - protected function getMigrationsDirectory(): string - { - return __DIR__ . DIRECTORY_SEPARATOR . - (new ReflectionClass($this))->getShortName() . DIRECTORY_SEPARATOR . - AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; - } - - protected function getMigrationsNamespace(): string - { - return self::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; - } - public function getPrefixedTableNameJobs(): string { return $this->prefixedTableNameJobs; @@ -179,4 +115,25 @@ class JobsStore implements JobsStoreInterface { return $this->prefixedTableNameFailedJobs; } + + /** + * Build store instance. + * @param ModuleConfiguration $moduleConfiguration + * @param LoggerInterface $logger + * @param string|null $connectionKey + * @return self + * @throws StoreException + */ + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self { + return new self( + $moduleConfiguration, + $logger, + new Factory($moduleConfiguration, $logger), + $connectionKey + ); + } } diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php b/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Bases/AbstractCreateJobsTable.php similarity index 58% rename from src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php rename to src/Stores/Jobs/DoctrineDbal/Store/Migrations/Bases/AbstractCreateJobsTable.php index 08a39ba94f094c879a8208211643757ab9df1acb..203afb8dfb7d2a0e09dbc3c2367f6da41032adee 100644 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php +++ b/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Bases/AbstractCreateJobsTable.php @@ -1,24 +1,23 @@ <?php -declare(strict_types=1); - -namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\TableConstants; use Throwable; -class Version20220601000100CreateFailedJobsTable extends AbstractMigration +abstract class AbstractCreateJobsTable extends AbstractMigration { /** * @throws MigrationException */ public function run(): void { - $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); + $tableName = $this->preparePrefixedTableName($this->getJobsTableName()); + try { $table = new Table($tableName); @@ -26,6 +25,9 @@ class Version20220601000100CreateFailedJobsTable extends AbstractMigration ->setUnsigned(true) ->setAutoincrement(true); + $table->addColumn('type', Types::STRING) + ->setLength(TableConstants::COLUMN_TYPE_LENGTH); + $table->addColumn('payload', Types::TEXT); $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); @@ -33,8 +35,10 @@ class Version20220601000100CreateFailedJobsTable extends AbstractMigration $this->schemaManager->createTable($table); } catch (Throwable $exception) { - $contextDetails = sprintf('Could not create table %s.', $tableName); - $this->throwGenericMigrationException($contextDetails, $exception); + throw $this->prepareGenericMigrationException( + \sprintf('Could not create table %s.', $tableName), + $exception + ); } } @@ -43,13 +47,14 @@ class Version20220601000100CreateFailedJobsTable extends AbstractMigration */ public function revert(): void { - $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); + $tableName = $this->preparePrefixedTableName($this->getJobsTableName()); try { $this->schemaManager->dropTable($tableName); } catch (Throwable $exception) { - $contextDetails = sprintf('Could not drop table %s.', $tableName); - $this->throwGenericMigrationException($contextDetails, $exception); + throw $this->prepareGenericMigrationException(\sprintf('Could not drop table %s.', $tableName), $exception); } } + + abstract protected function getJobsTableName(): string; } diff --git a/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000000CreateJobTable.php b/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000000CreateJobTable.php new file mode 100644 index 0000000000000000000000000000000000000000..838b202bcb7403e608126e9c32f70fd62d4c79ef --- /dev/null +++ b/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000000CreateJobTable.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations; + +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; + +class Version20220601000000CreateJobTable extends Store\Migrations\Bases\AbstractCreateJobsTable +{ + protected function getJobsTableName(): string + { + return 'job'; + } +} diff --git a/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000100CreateJobFailedTable.php b/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000100CreateJobFailedTable.php new file mode 100644 index 0000000000000000000000000000000000000000..9aa852dc9bdd08fb3813d87c539745090e4936a7 --- /dev/null +++ b/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000100CreateJobFailedTable.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations; + +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; + +class Version20220601000100CreateJobFailedTable extends Store\Migrations\Bases\AbstractCreateJobsTable +{ + protected function getJobsTableName(): string + { + return 'job_failed'; + } +} diff --git a/src/Stores/Jobs/DoctrineDbal/Store/RawJob.php b/src/Stores/Jobs/DoctrineDbal/Store/RawJob.php new file mode 100644 index 0000000000000000000000000000000000000000..5737b9230750963ae0c7bad7514d97deb1429261 --- /dev/null +++ b/src/Stores/Jobs/DoctrineDbal/Store/RawJob.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; + +use DateTimeImmutable; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use Throwable; + +use function sprintf; + +class RawJob extends AbstractRawEntity +{ + protected int $id; + protected AbstractPayload $payload; + protected string $type; + protected DateTimeImmutable $createdAt; + + public function __construct(array $rawRow, AbstractPlatform $abstractPlatform) + { + parent::__construct($rawRow, $abstractPlatform); + + $this->id = (int)$rawRow[Store\TableConstants::COLUMN_NAME_ID]; + $this->payload = $this->resolvePayload((string)$rawRow[Store\TableConstants::COLUMN_NAME_PAYLOAD]); + $this->type = (string)$rawRow[Store\TableConstants::COLUMN_NAME_TYPE]; + $this->createdAt = $this->resolveDateTimeImmutable($rawRow[Store\TableConstants::COLUMN_NAME_CREATED_AT]); + } + + protected function validate(array $rawRow): void + { + $columnsToCheck = [ + Store\TableConstants::COLUMN_NAME_ID, + Store\TableConstants::COLUMN_NAME_PAYLOAD, + Store\TableConstants::COLUMN_NAME_TYPE, + Store\TableConstants::COLUMN_NAME_CREATED_AT, + ]; + + foreach ($columnsToCheck as $column) { + if (empty($rawRow[$column])) { + throw new UnexpectedValueException(sprintf('Column %s must be set.', $column)); + } + } + + if (! is_numeric($rawRow[Store\TableConstants::COLUMN_NAME_ID])) { + throw new UnexpectedValueException( + sprintf('Column %s must be numeric.', Store\TableConstants::COLUMN_NAME_ID) + ); + } + + if (! is_string($rawRow[Store\TableConstants::COLUMN_NAME_PAYLOAD])) { + throw new UnexpectedValueException( + sprintf('Column %s must be string.', Store\TableConstants::COLUMN_NAME_PAYLOAD) + ); + } + + if (! is_string($rawRow[Store\TableConstants::COLUMN_NAME_TYPE])) { + throw new UnexpectedValueException( + sprintf('Column %s must be string.', Store\TableConstants::COLUMN_NAME_TYPE) + ); + } + + if (! is_string($rawRow[Store\TableConstants::COLUMN_NAME_CREATED_AT])) { + throw new UnexpectedValueException( + sprintf('Column %s must be string.', Store\TableConstants::COLUMN_NAME_CREATED_AT) + ); + } + } + + protected function resolvePayload(string $rawPayload): AbstractPayload + { + /** @psalm-suppress MixedAssignment - we check the type manually */ + $payload = unserialize($rawPayload); + + if ($payload instanceof AbstractPayload) { + return $payload; + } + + throw new UnexpectedValueException('Job payload is not instance of AbstractPayload.'); + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @return AbstractPayload + */ + public function getPayload(): AbstractPayload + { + return $this->payload; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return DateTimeImmutable + */ + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } +} diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Repository.php b/src/Stores/Jobs/DoctrineDbal/Store/Repository.php similarity index 72% rename from src/Stores/Jobs/DoctrineDbal/JobsStore/Repository.php rename to src/Stores/Jobs/DoctrineDbal/Store/Repository.php index 2c06c20276f208a18e250cf63404944f8ad59f80..ac983a76fa6e004411e65ebefdec50da5af939ee 100644 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Repository.php +++ b/src/Stores/Jobs/DoctrineDbal/Store/Repository.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; use Doctrine\DBAL\Types\Types; use Psr\Log\LoggerInterface; @@ -10,9 +10,8 @@ use ReflectionClass; use SimpleSAML\Module\accounting\Entities\GenericJob; use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; use SimpleSAML\Module\accounting\Exceptions\StoreException; -use SimpleSAML\Module\accounting\Services\LoggerService; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; use Throwable; class Repository @@ -24,6 +23,9 @@ class Repository protected string $tableName; protected LoggerInterface $logger; + /** + * @throws StoreException + */ public function __construct(Connection $connection, string $tableName, LoggerInterface $logger) { $this->connection = $connection; @@ -39,9 +41,9 @@ class Repository protected function prepareValidJobsTableNames(): void { $this->validJobsTableNames[] = $this->connection - ->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); + ->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); $this->validJobsTableNames[] = $this->connection - ->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); + ->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB_FAILED); } /** @@ -56,21 +58,21 @@ class Repository $queryBuilder->insert($this->tableName) ->values( [ - JobsStore::COLUMN_NAME_PAYLOAD => ':' . JobsStore::COLUMN_NAME_PAYLOAD, - JobsStore::COLUMN_NAME_TYPE => ':' . JobsStore::COLUMN_NAME_TYPE, - JobsStore::COLUMN_NAME_CREATED_AT => ':' . JobsStore::COLUMN_NAME_CREATED_AT, + Store\TableConstants::COLUMN_NAME_PAYLOAD => ':' . Store\TableConstants::COLUMN_NAME_PAYLOAD, + Store\TableConstants::COLUMN_NAME_TYPE => ':' . Store\TableConstants::COLUMN_NAME_TYPE, + Store\TableConstants::COLUMN_NAME_CREATED_AT => ':' . Store\TableConstants::COLUMN_NAME_CREATED_AT, ] ) ->setParameters( [ - JobsStore::COLUMN_NAME_PAYLOAD => serialize($job->getPayload()), - JobsStore::COLUMN_NAME_TYPE => $job->getType(), - JobsStore::COLUMN_NAME_CREATED_AT => $job->getCreatedAt(), + Store\TableConstants::COLUMN_NAME_PAYLOAD => serialize($job->getPayload()), + Store\TableConstants::COLUMN_NAME_TYPE => $job->getType(), + Store\TableConstants::COLUMN_NAME_CREATED_AT => $job->getCreatedAt(), ], [ - JobsStore::COLUMN_NAME_PAYLOAD => Types::TEXT, - JobsStore::COLUMN_NAME_TYPE => Types::STRING, - JobsStore::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + Store\TableConstants::COLUMN_NAME_PAYLOAD => Types::TEXT, + Store\TableConstants::COLUMN_NAME_TYPE => Types::STRING, + Store\TableConstants::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE ] ); @@ -95,17 +97,19 @@ class Repository * @psalm-suppress TooManyArguments - providing array or null is deprecated */ $queryBuilder->select( - JobsStore::COLUMN_NAME_ID, - JobsStore::COLUMN_NAME_PAYLOAD, - JobsStore::COLUMN_NAME_TYPE, - JobsStore::COLUMN_NAME_CREATED_AT + Store\TableConstants::COLUMN_NAME_ID, + Store\TableConstants::COLUMN_NAME_PAYLOAD, + Store\TableConstants::COLUMN_NAME_TYPE, + Store\TableConstants::COLUMN_NAME_CREATED_AT ) ->from($this->tableName) - ->orderBy(JobsStore::COLUMN_NAME_ID) + ->orderBy(Store\TableConstants::COLUMN_NAME_ID) ->setMaxResults(1); if ($type !== null) { - $queryBuilder->where(JobsStore::COLUMN_NAME_TYPE . ' = ' . $queryBuilder->createNamedParameter($type)); + $queryBuilder->where( + Store\TableConstants::COLUMN_NAME_TYPE . ' = ' . $queryBuilder->createNamedParameter($type) + ); } try { @@ -149,8 +153,8 @@ class Repository $numberOfAffectedRows = (int)$this->connection->dbal() ->delete( $this->tableName, - [JobsStore::COLUMN_NAME_ID => $id], - [JobsStore::COLUMN_NAME_ID => Types::BIGINT] + [Store\TableConstants::COLUMN_NAME_ID => $id], + [Store\TableConstants::COLUMN_NAME_ID => Types::BIGINT] ); } catch (Throwable $exception) { $message = sprintf('Error while trying to delete a job with ID %s.', $id); @@ -181,9 +185,9 @@ class Repository */ protected function validateType(string $type): void { - if (mb_strlen($type) > JobsStore::COLUMN_LENGTH_TYPE) { + if (mb_strlen($type) > Store\TableConstants::COLUMN_TYPE_LENGTH) { throw new StoreException( - sprintf('String length for type column exceeds %s limit.', JobsStore::COLUMN_LENGTH_TYPE) + sprintf('String length for type column exceeds %s limit.', Store\TableConstants::COLUMN_TYPE_LENGTH) ); } } diff --git a/src/Stores/Jobs/DoctrineDbal/Store/TableConstants.php b/src/Stores/Jobs/DoctrineDbal/Store/TableConstants.php new file mode 100644 index 0000000000000000000000000000000000000000..55f1779217f7d8467ac5e2cb775c6a9c60920c99 --- /dev/null +++ b/src/Stores/Jobs/DoctrineDbal/Store/TableConstants.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; + +class TableConstants +{ + public const TABLE_NAME_JOB = 'job'; + public const TABLE_NAME_JOB_FAILED = 'job_failed'; + + // Both tables have same columns. + public const COLUMN_NAME_ID = 'id'; + public const COLUMN_NAME_PAYLOAD = 'payload'; + public const COLUMN_NAME_TYPE = 'type'; + public const COLUMN_NAME_CREATED_AT = 'created_at'; + + public const COLUMN_TYPE_LENGTH = 1024; +} diff --git a/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php b/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..3a9c8d2a06f7fb158143a28af2393e200fa5a0d7 --- /dev/null +++ b/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Activity; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Helpers\HashHelper; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers\Interfaces\AuthenticationDataProviderInterface; +use SimpleSAML\Module\accounting\Stores\Builders\DataStoreBuilder; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; +use SimpleSAML\Module\accounting\Stores\Interfaces\DataStoreInterface; +use SimpleSAML\Module\accounting\Trackers\Interfaces\AuthenticationDataTrackerInterface; + +class Tracker implements AuthenticationDataTrackerInterface, AuthenticationDataProviderInterface +{ + protected ModuleConfiguration $moduleConfiguration; + protected LoggerInterface $logger; + protected DataStoreInterface $dataStore; + + public function __construct( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + DataStoreInterface $dataStore = null + ) { + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + + // Use provided store or initialize default store for this tracker. + $this->dataStore = $dataStore ?? + (new DataStoreBuilder($this->moduleConfiguration, $this->logger)) + ->build(Store::class, $this->moduleConfiguration->getClassConnectionKey(self::class)); + } + + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger + ): self { + return new self($moduleConfiguration, $logger); + } + + public function process(Event $authenticationEvent): void + { + $this->dataStore->persist($authenticationEvent); + } + + public function needsSetup(): bool + { + return $this->dataStore->needsSetup(); + } + + public function runSetup(): void + { + $this->dataStore->runSetup(); + } + + public function getConnectedServiceProviders(string $userIdentifier): ConnectedServiceProvider\Bag + { + $userIdentifierHashSha256 = HashHelper::getSha256($userIdentifier); + return $this->dataStore->getConnectedOrganizations($userIdentifierHashSha256); + } + + public function getActivity(string $userIdentifier): Activity\Bag + { + $userIdentifierHashSha256 = HashHelper::getSha256($userIdentifier); + return $this->dataStore->getActivity($userIdentifierHashSha256); + } +} diff --git a/src/Trackers/Builders/AuthenticationDataTrackerBuilder.php b/src/Trackers/Builders/AuthenticationDataTrackerBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..e584a6c9f8ed930200ea2a4087f4b00599b922ee --- /dev/null +++ b/src/Trackers/Builders/AuthenticationDataTrackerBuilder.php @@ -0,0 +1,54 @@ +<?php + +namespace SimpleSAML\Module\accounting\Trackers\Builders; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\Exception; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Trackers\Interfaces\AuthenticationDataTrackerInterface; +use Throwable; + +class AuthenticationDataTrackerBuilder +{ + protected ModuleConfiguration $moduleConfiguration; + protected LoggerInterface $logger; + + public function __construct(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger) + { + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + } + + /** + * @throws Exception + */ + public function build(string $class): AuthenticationDataTrackerInterface + { + try { + // Make sure that the class implements proper interface + if (!is_subclass_of($class, AuthenticationDataTrackerInterface::class)) { + $message = sprintf( + 'Class %s does not implement interface %s.', + $class, + AuthenticationDataTrackerInterface::class + ); + throw new UnexpectedValueException($message); + } + + // Build... + /** @var AuthenticationDataTrackerInterface $store */ + $store = InstanceBuilderUsingModuleConfigurationHelper::build( + $class, + $this->moduleConfiguration, + $this->logger + ); + } catch (Throwable $exception) { + $message = sprintf('Error building instance for class %s. Error was: %s', $class, $exception->getMessage()); + throw new Exception($message, (int)$exception->getCode(), $exception); + } + + return $store; + } +} diff --git a/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php b/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..b3b95720279ab21603920b762477553244593833 --- /dev/null +++ b/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php @@ -0,0 +1,17 @@ +<?php + +namespace SimpleSAML\Module\accounting\Trackers\Interfaces; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\Interfaces\SetupableInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers\Interfaces\AuthenticationDataProviderInterface; + +interface AuthenticationDataTrackerInterface extends BuildableUsingModuleConfigurationInterface, SetupableInterface +{ + public static function build(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger): self; + + public function process(Event $authenticationEvent): void; +} diff --git a/templates/admin/configuration/status.twig b/templates/admin/configuration/status.twig new file mode 100644 index 0000000000000000000000000000000000000000..1ce05e9439de2df6715aa22048810fe8ba3ea62f --- /dev/null +++ b/templates/admin/configuration/status.twig @@ -0,0 +1,47 @@ +{# @var moduleConfiguration \SimpleSAML\Module\accounting\ModuleConfiguration #} + +{% set pagetitle = 'Configuration Status'|trans %} +{% set frontpage_section = 'main' %} + +{% extends "base.twig" %} + +{% block preload %} + <link rel="stylesheet" href="{{ asset('css/accounting.css', 'accounting') }}"> +{% endblock %} + +{% block content %} + + <h2>{{ pagetitle }} </h2> + + {% if configurationValidationErrors is not null %} + <p>{{ configurationValidationErrors }}</p> + {% elseif moduleConfiguration is not null %} + <ul> + <li> + <strong>{{ 'User ID Attribute Name'|trans }}</strong>: {{ moduleConfiguration.getUserIdAttributeName }} + </li> + <li> + <strong>{{ 'Accounting Processing Type'|trans }}</strong>: {{ moduleConfiguration.getAccountingProcessingType }} + </li> + </ul> + + {% if moduleConfiguration.getAccountingProcessingType == 'asynchronous' %} + <ul> + <li> + <strong>{{ 'Jobs Store Class'|trans }}</strong>: {{ moduleConfiguration.getJobsStoreClass }} + </li> + <li> + {% if jobsStore is not null %} + <strong>{{ 'Jobs Store Setup Needed'|trans }}</strong>: + {{ jobsStore.needsSetup ? 'Yes'|trans : 'No'|trans }} + {% else %} + {{ 'Could not initialize jobs store.'|trans }} + {% endif %} + </li> + </ul> + {% endif %} + {% else %} + <p>{{ 'Could not initialize module configuration.'|trans }}</p> + {% endif %} + +{% endblock %} diff --git a/templates/configuration.twig b/templates/configuration.twig deleted file mode 100644 index 78db9bda537ad5040df993efceb8fd746d6183d3..0000000000000000000000000000000000000000 --- a/templates/configuration.twig +++ /dev/null @@ -1,19 +0,0 @@ -{% set pagetitle = 'Accounting configuration page'|trans %} -{% set frontpage_section = 'main' %} - -{% extends "base.twig" %} - -{% block preload %} - <link rel="stylesheet" href="{{ asset('css/accounting.css', 'accounting') }}"> -{% endblock %} - -{% block content %} - - <h2>{{ pagetitle }} </h2> - - <p> - {{ test }}, <br> - {{ test_config }} - </p> - -{% endblock %} diff --git a/templates/test.twig b/templates/test.twig new file mode 100644 index 0000000000000000000000000000000000000000..59615eefefbd7da302031730e709102b3c79b9e3 --- /dev/null +++ b/templates/test.twig @@ -0,0 +1,25 @@ +{% set pagetitle = 'Setup'|trans %} +{% set frontpage_section = 'main' %} + +{% extends "base.twig" %} + +{% block preload %} + <link rel="stylesheet" href="{{ asset('css/accounting.css', 'accounting') }}"> +{% endblock %} + +{% block content %} + + <h2>{{ pagetitle }} </h2> + + <p> + Jobs Store class: {{ jobs_store }}, <br> + Jobs Store needs setup: {{ jobs_store_needs_setup }}, <br> + Jobs Store setup ran: {{ jobs_store_setup_ran }}, <br> + + Data Store class: {{ data_store }}, <br> + Data Store needs setup: {{ data_store_needs_setup }}, <br> + Data Store setup ran: {{ data_store_setup_ran }} <br> + </p> + +{% endblock %} +{# TODO mivanci delete this file #} \ No newline at end of file diff --git a/templates/user/connected-organizations.twig b/templates/user/connected-organizations.twig new file mode 100644 index 0000000000000000000000000000000000000000..a05508ea7746936b5eacf9dad7a6f3c1d6212c9a --- /dev/null +++ b/templates/user/connected-organizations.twig @@ -0,0 +1,24 @@ +{# @var moduleConfiguration \SimpleSAML\Module\accounting\ModuleConfiguration #} + +{% set pagetitle = 'Connected Organizations'|trans %} +{% set frontpage_section = 'main' %} + +{% extends "base.twig" %} + +{% block preload %} + <link rel="stylesheet" href="{{ asset('css/accounting.css', 'accounting') }}"> +{% endblock %} + +{% block content %} + + <h2>{{ pagetitle }} </h2> + + + + <ul> + {% for name, value in normalizedAttributes %} + <li><strong>{{ name }}</strong>: {{ value }}</li> + {% endfor %} + </ul> + +{% endblock %} diff --git a/templates/user/personal-data.twig b/templates/user/personal-data.twig new file mode 100644 index 0000000000000000000000000000000000000000..51a0921f26e8de58817e6ee5a1f7f14758d11716 --- /dev/null +++ b/templates/user/personal-data.twig @@ -0,0 +1,26 @@ +{# @var moduleConfiguration \SimpleSAML\Module\accounting\ModuleConfiguration #} + +{% set pagetitle = 'Personal Data'|trans %} +{% set frontpage_section = 'main' %} + +{% extends "base.twig" %} + +{% block preload %} + <link rel="stylesheet" href="{{ asset('css/accounting.css', 'accounting') }}"> +{% endblock %} + +{% block content %} + + <h2>{{ pagetitle }} </h2> + + <p> + {% trans %}This is what we know about you... Lorem ipsum...{% endtrans %} + </p> + + <ul> + {% for name, value in normalizedAttributes %} + <li><strong>{{ name }}</strong>: {{ value }}</li> + {% endfor %} + </ul> + +{% endblock %} diff --git a/tests/attributemap/test.php b/tests/attributemap/test.php new file mode 100644 index 0000000000000000000000000000000000000000..3b756fa4f05d213e126c8b333628230cfd30a452 --- /dev/null +++ b/tests/attributemap/test.php @@ -0,0 +1,8 @@ +<?php +// phpcs:ignoreFile + +declare(strict_types=1); + +$attributemap = [ + 'mobile' => 'urn:mace:dir:attribute-def:mobile' +]; diff --git a/tests/attributemap/test2.php b/tests/attributemap/test2.php new file mode 100644 index 0000000000000000000000000000000000000000..6d4443e7b0c97d2244356b8a562491bb1b836561 --- /dev/null +++ b/tests/attributemap/test2.php @@ -0,0 +1,8 @@ +<?php +// phpcs:ignoreFile + +declare(strict_types=1); + +$attributemap = [ + 'phone' => 'urn:mace:dir:attribute-def:phone' +]; diff --git a/tests/config-templates/config.php b/tests/config-templates/config.php index c27decf2ca22267f2a3549033d38d2019bb7055f..a6c0417abcb66585642837aa25834dd82109c006 100644 --- a/tests/config-templates/config.php +++ b/tests/config-templates/config.php @@ -902,7 +902,7 @@ $config = [ *************************************/ /* - * Authentication processing filters that will be executed for all IdPs + * Tracker processing filters that will be executed for all IdPs */ 'authproc.idp' => [ /* Enable the authproc filter below to add URN prefixes to all attributes @@ -957,7 +957,7 @@ $config = [ ], /* - * Authentication processing filters that will be executed for all SPs + * Tracker processing filters that will be executed for all SPs */ 'authproc.sp' => [ /* diff --git a/tests/config-templates/invalid_array_value_module_accounting.php b/tests/config-templates/invalid_array_value_module_accounting.php new file mode 100644 index 0000000000000000000000000000000000000000..b5f3c8a7bca75dc435b7b801a1e48d989dc0766e --- /dev/null +++ b/tests/config-templates/invalid_array_value_module_accounting.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; + +$config = [ + ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME => 'urn:oasis:names:tc:SAML:attribute:subject-id', + + ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => + ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS, + + ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\Store::class, + + ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class, + + ModuleConfiguration::OPTION_CLASS_TO_CONNECTION_MAP => [ + 'invalid-array-value' => [ + 'no-master-key' => 'invalid', + ], + ], + ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ + 'invalid', + ], + + ModuleConfiguration::OPTION_CONNECTIONS_AND_PARAMETERS => [ + 'doctrine_dbal_pdo_mysql' => [ + 'driver' => 'pdo_mysql', + ], + 'doctrine_dbal_pdo_sqlite' => [ + 'driver' => 'pdo_sqlite', + ], + ], +]; diff --git a/tests/config-templates/invalid_async_module_accounting.php b/tests/config-templates/invalid_async_module_accounting.php new file mode 100644 index 0000000000000000000000000000000000000000..77a3517abfce30e0a334f063a1ecea333492a065 --- /dev/null +++ b/tests/config-templates/invalid_async_module_accounting.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; + +$config = [ + ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME => 'urn:oasis:names:tc:SAML:attribute:subject-id', + + ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => + ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS, + + ModuleConfiguration::OPTION_JOBS_STORE => 'invalid', + + ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => 'invalid', + + ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ + 'invalid', + ], + + ModuleConfiguration::OPTION_CLASS_TO_CONNECTION_MAP => [ + 'invalid' + ], + + ModuleConfiguration::OPTION_CONNECTIONS_AND_PARAMETERS => [ + 'doctrine_dbal_pdo_mysql' => [ + 'driver' => 'pdo_mysql', + ], + 'doctrine_dbal_pdo_sqlite' => [ + 'driver' => 'pdo_sqlite', + ], + ], +]; diff --git a/tests/config-templates/invalid_module_accounting.php b/tests/config-templates/invalid_module_accounting.php new file mode 100644 index 0000000000000000000000000000000000000000..5a700dbcb4ea13102ed5a7bd1fe871cfa5109910 --- /dev/null +++ b/tests/config-templates/invalid_module_accounting.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; + +$config = [ + ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME => 'urn:oasis:names:tc:SAML:attribute:subject-id', + + ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => 'invalid', + + ModuleConfiguration::OPTION_JOBS_STORE => 'invalid', + + ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => 'invalid', + + ModuleConfiguration::OPTION_CLASS_TO_CONNECTION_MAP => [ + /** + * Connection key to be used by jobs store class. + */ + Stores\Jobs\DoctrineDbal\Store::class => 'invalid', + /** + * Connection key to be used by this data tracker and provider. + */ + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class => [ + ModuleConfiguration\ConnectionType::MASTER => 'invalid', + ModuleConfiguration\ConnectionType::SLAVE => [ + 'invalid', + ], + ], + ], + ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ + 'invalid', + ['invalid'] + ], + + ModuleConfiguration::OPTION_CONNECTIONS_AND_PARAMETERS => [ + 'doctrine_dbal_pdo_mysql' => [ + 'driver' => 'pdo_mysql', + ], + 'doctrine_dbal_pdo_sqlite' => [ + 'driver' => 'pdo_sqlite', + ], + ], +]; diff --git a/tests/config-templates/invalid_object_value_module_accounting.php b/tests/config-templates/invalid_object_value_module_accounting.php new file mode 100644 index 0000000000000000000000000000000000000000..6c46610918610bac25f3adc7507da16c88cf84dc --- /dev/null +++ b/tests/config-templates/invalid_object_value_module_accounting.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; + +$config = [ + ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME => 'urn:oasis:names:tc:SAML:attribute:subject-id', + + ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => + ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS, + + ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\Store::class, + + ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class, + + ModuleConfiguration::OPTION_CLASS_TO_CONNECTION_MAP => [ + 'invalid-object-value' => new stdClass(), + ], + ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ + 'invalid', + ], + + ModuleConfiguration::OPTION_CONNECTIONS_AND_PARAMETERS => [ + 'doctrine_dbal_pdo_mysql' => [ + 'driver' => 'pdo_mysql', + ], + 'doctrine_dbal_pdo_sqlite' => [ + 'driver' => 'pdo_sqlite', + ], + ], +]; diff --git a/tests/config-templates/module_accounting.php b/tests/config-templates/module_accounting.php index e0d7108165d0f8a13e7a6cce8ce5ef21c75058c7..4eec5cf688732bbad540a467bffcf384fbcdfeb3 100644 --- a/tests/config-templates/module_accounting.php +++ b/tests/config-templates/module_accounting.php @@ -3,74 +3,48 @@ declare(strict_types=1); use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers; use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; $config = [ - /** - * User ID attribute, one that is always available and that is unique to all users. - * If this attribute is not available, accounting will not be performed for that user. - * - * Examples: - * urn:oasis:names:tc:SAML:attribute:subject-id - * eduPersonUniqueId - * eduPersonPrincipalName - */ - ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE => 'urn:oasis:names:tc:SAML:attribute:subject-id', - /** - * Accounting processing type. There are two possible types: 'synchronous' and 'asynchronous'. - * - 'synchronous': accounting processing will be performed during authentication itself (slower) - * - 'asynchronous': for each authentication event a new job will be created for later processing (faster, - * but requires setting up job storage and a cron entry). - */ + ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME => 'urn:oasis:names:tc:SAML:attribute:subject-id', + + ModuleConfiguration::OPTION_DEFAULT_AUTHENTICATION_SOURCE => 'default-sp', + ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS, - /** - * Jobs store. Determines which of the available stores will be used to store jobs in case the 'asynchronous' - * accounting processing type was set. - */ - ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\JobsStore::class, + ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\Store::class, + + ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class, - /** - * Store connection for particular store. Can be used to set different connections for different stores. - */ - ModuleConfiguration::OPTION_STORE_TO_CONNECTION_KEY_MAP => [ - Stores\Jobs\DoctrineDbal\JobsStore::class => 'doctrine_dbal_pdo_mysql', + ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ + // ], - /** - * Store connections and their parameters. - * - * Any compatible Doctrine DBAL implementation can be used: - * https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html - * There are additional parameters for: table prefix. - * Examples for mysql and sqlite are provided below. - */ - ModuleConfiguration::OPTION_ALL_STORE_CONNECTIONS_AND_PARAMETERS => [ - 'doctrine_dbal_pdo_mysql' => [ - 'driver' => 'pdo_mysql', // (string): The built-in driver implementation to use - 'user' => 'user', // (string): Username to use when connecting to the database. - 'password' => 'password', // (string): Password to use when connecting to the database. - 'host' => 'host', // (string): Hostname of the database to connect to. - 'port' => 3306, // (integer): Port of the database to connect to. - 'dbname' => 'dbname', // (string): Name of the database/schema to connect to. - //'unix_socket' => 'unix_socket', // (string): Name of the socket used to connect to the database. - 'charset' => 'utf8', // (string): The charset used when connecting to the database. - //'url' => 'mysql://user:secret@localhost/mydb?charset=utf8', // ...alternative way of providing parameters. - // Additional parameters not originally available in Doctrine DBAL - 'table_prefix' => '', // (string): Prefix for each table. + ModuleConfiguration::OPTION_CLASS_TO_CONNECTION_MAP => [ + Stores\Jobs\DoctrineDbal\Store::class => 'doctrine_dbal_pdo_sqlite', + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class => [ + ModuleConfiguration\ConnectionType::MASTER => 'doctrine_dbal_pdo_sqlite', + ModuleConfiguration\ConnectionType::SLAVE => [ + 'doctrine_dbal_pdo_sqlite_slave', + ], ], + ], + + ModuleConfiguration::OPTION_CONNECTIONS_AND_PARAMETERS => [ 'doctrine_dbal_pdo_sqlite' => [ - 'driver' => 'pdo_sqlite', // (string): The built-in driver implementation to use - 'path' => '/path/to/db.sqlite', // (string): The filesystem path to the database file. - // Mutually exclusive with memory. path takes precedence. - 'memory' => false, // (boolean): True if the SQLite database should be in-memory (non-persistent). - // Mutually exclusive with path. path takes precedence. - //'url' => 'sqlite:////path/to/db.sqlite // ...alternative way of providing path parameter. - //'url' => 'sqlite:///:memory:' // ...alternative way of providing memory parameter. - // Additional parameters not originally available in Doctrine DBAL - 'table_prefix' => '', // (string): Prefix for each table. + 'driver' => 'pdo_sqlite', + 'memory' => true, + 'table_prefix' => '', + ], + 'doctrine_dbal_pdo_sqlite_slave' => [ + 'driver' => 'pdo_sqlite', + 'memory' => true, + 'table_prefix' => '', ], ], ]; diff --git a/tests/src/Auth/Process/AccountingTest.php b/tests/src/Auth/Process/AccountingTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c00dbfdc7478efe66a40a72001ba8bb845cef0fa --- /dev/null +++ b/tests/src/Auth/Process/AccountingTest.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Auth\Process; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Auth\Process\Accounting; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker; +use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; + +/** + * @covers \SimpleSAML\Module\accounting\Auth\Process\Accounting + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Builders\Bases\AbstractStoreBuilder + * @uses \SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event\Job + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper + */ +class AccountingTest extends TestCase +{ + protected \PHPUnit\Framework\MockObject\Stub $moduleConfigurationStub; + protected \PHPUnit\Framework\MockObject\MockObject $loggerMock; + protected array $filterConfig; + protected \PHPUnit\Framework\MockObject\MockObject $jobsStoreBuilderMock; + protected \PHPUnit\Framework\MockObject\MockObject $authenticationDataTrackerBuilderMock; + protected \PHPUnit\Framework\MockObject\MockObject $jobsStoreMock; + protected \PHPUnit\Framework\MockObject\MockObject $trackerMock; + protected array $sampleState; + + public function testCanCreateInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf( + Accounting::class, + new Accounting( + $this->filterConfig, + null, + $this->moduleConfigurationStub, + $this->loggerMock, + ) + ); + + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf( + Accounting::class, + new Accounting( + $this->filterConfig, + null, + $this->moduleConfigurationStub, + $this->loggerMock, + $this->jobsStoreBuilderMock, + $this->authenticationDataTrackerBuilderMock + ) + ); + } + + public function testCreatesJobOnAsynchronousAccountingType(): void + { + $this->moduleConfigurationStub->method('getAccountingProcessingType') + ->willReturn(ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS); + $this->moduleConfigurationStub->method('getJobsStoreClass') + ->willReturn(Store::class); + + $this->jobsStoreMock->expects($this->once()) + ->method('enqueue') + ->with($this->isInstanceOf(Event\Job::class)); + + $this->jobsStoreBuilderMock->expects($this->once()) + ->method('build') + ->with($this->equalTo(Store::class)) + ->willReturn($this->jobsStoreMock); + + $this->authenticationDataTrackerBuilderMock + ->expects($this->never()) + ->method('build'); + + /** @psalm-suppress InvalidArgument */ + (new Accounting( + $this->filterConfig, + null, + $this->moduleConfigurationStub, + $this->loggerMock, + $this->jobsStoreBuilderMock, + $this->authenticationDataTrackerBuilderMock + ))->process($this->sampleState); + } + + public function testAccountingRunsOnSynchronousType(): void + { + $this->moduleConfigurationStub->method('getAccountingProcessingType') + ->willReturn(ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS); + + $this->moduleConfigurationStub->method('getDefaultDataTrackerAndProviderClass') + ->willReturn(Tracker::class); + $this->moduleConfigurationStub->method('getAdditionalTrackers')->willReturn([]); + + $this->jobsStoreBuilderMock->expects($this->never()) + ->method('build'); + + $this->trackerMock + ->expects($this->once()) + ->method('process') + ->with($this->isInstanceOf(Event::class)); + + $this->authenticationDataTrackerBuilderMock + ->expects($this->once()) + ->method('build') + ->with($this->equalTo(Tracker::class)) + ->willReturn($this->trackerMock); + + /** @psalm-suppress InvalidArgument */ + (new Accounting( + $this->filterConfig, + null, + $this->moduleConfigurationStub, + $this->loggerMock, + $this->jobsStoreBuilderMock, + $this->authenticationDataTrackerBuilderMock + ))->process($this->sampleState); + } + + public function testLogsErrorOnException(): void + { + $this->moduleConfigurationStub->method('getAccountingProcessingType') + ->willThrowException(new InvalidConfigurationException('test')); + + $this->loggerMock->expects($this->once())->method('error'); + + /** @psalm-suppress InvalidArgument */ + (new Accounting( + $this->filterConfig, + null, + $this->moduleConfigurationStub, + $this->loggerMock, + $this->jobsStoreBuilderMock, + $this->authenticationDataTrackerBuilderMock + ))->process($this->sampleState); + } + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->jobsStoreBuilderMock = $this->createMock(JobsStoreBuilder::class); + $this->authenticationDataTrackerBuilderMock = + $this->createMock(AuthenticationDataTrackerBuilder::class); + + $this->jobsStoreMock = $this->createMock(Store::class); + $this->trackerMock = $this->createMock(Tracker::class); + + $this->sampleState = StateArrays::FULL; + + $this->filterConfig = []; + } +} diff --git a/tests/src/Constants/ConnectionParameters.php b/tests/src/Constants/ConnectionParameters.php new file mode 100644 index 0000000000000000000000000000000000000000..9aad77aa5077731e68360b81c282e602c002dca8 --- /dev/null +++ b/tests/src/Constants/ConnectionParameters.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Constants; + +class ConnectionParameters +{ + public const DBAL_SQLITE_MEMORY = ['driver' => 'pdo_sqlite', 'memory' => true,]; +} diff --git a/tests/src/Constants/DateTime.php b/tests/src/Constants/DateTime.php new file mode 100644 index 0000000000000000000000000000000000000000..df07b1951637fed22e89289a63a664846db6a112 --- /dev/null +++ b/tests/src/Constants/DateTime.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Constants; + +final class DateTime +{ + public const DEFAULT_FORMAT = 'Y-m-d H:i:s'; +} diff --git a/tests/src/Constants/RawRowResult.php b/tests/src/Constants/RawRowResult.php new file mode 100644 index 0000000000000000000000000000000000000000..f09e6fc597070ad9b05401e60fce97c1d276979d --- /dev/null +++ b/tests/src/Constants/RawRowResult.php @@ -0,0 +1,26 @@ +<?php +// phpcs:ignoreFile + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Constants; + +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; + +class RawRowResult +{ + public const CONNECTED_ORGANIZATION = [ + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS => 1, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT => '2022-02-22 22:22:22', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT => '2022-02-02 22:22:22', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA => 'a:9:{s:19:"SingleLogoutService";a:1:{i:0;a:2:{s:7:"Binding";s:50:"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";s:8:"Location";s:121:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp";}}s:24:"AssertionConsumerService";a:2:{i:0;a:3:{s:7:"Binding";s:46:"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";s:8:"Location";s:126:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp";s:5:"index";i:0;}i:1;a:3:{s:7:"Binding";s:50:"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact";s:8:"Location";s:126:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp";s:5:"index";i:1;}}s:8:"contacts";a:1:{i:0;a:3:{s:12:"emailAddress";s:15:"mivanci@srce.hr";s:9:"givenName";s:15:"Marko Ivančić";s:11:"contactType";s:9:"technical";}}s:4:"name";a:1:{s:2:"en";s:12:"Test service";}s:11:"description";a:1:{s:2:"en";s:27:"Description of test service";}s:16:"OrganizationName";a:1:{s:2:"en";s:17:"Test organization";}s:8:"entityid";s:114:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp";s:14:"metadata-index";s:114:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp";s:12:"metadata-set";s:16:"saml20-sp-remote";}', + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES => 'a:5:{s:33:"urn:oid:0.9.2342.19200300.100.1.1";a:1:{i:0;s:11:"student-uid";}s:32:"urn:oid:1.3.6.1.4.1.5923.1.1.1.1";a:2:{i:0;s:6:"member";i:1;s:7:"student";}s:15:"urn:oid:2.5.4.4";a:1:{i:0;s:10:"student-sn";}s:19:"hrEduPersonUniqueID";a:1:{i:0;s:19:"student@example.org";}s:23:"hrEduPersonPersistentID";a:1:{i:0;s:13:"student123abc";}}', + ]; + + + public const ACTIVITY = [ + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT => '2022-02-22 22:22:22', + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA => 'a:9:{s:19:"SingleLogoutService";a:1:{i:0;a:2:{s:7:"Binding";s:50:"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";s:8:"Location";s:121:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp";}}s:24:"AssertionConsumerService";a:2:{i:0;a:3:{s:7:"Binding";s:46:"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";s:8:"Location";s:126:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp";s:5:"index";i:0;}i:1;a:3:{s:7:"Binding";s:50:"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact";s:8:"Location";s:126:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp";s:5:"index";i:1;}}s:8:"contacts";a:1:{i:0;a:3:{s:12:"emailAddress";s:15:"mivanci@srce.hr";s:9:"givenName";s:15:"Marko Ivančić";s:11:"contactType";s:9:"technical";}}s:4:"name";a:1:{s:2:"en";s:12:"Test service";}s:11:"description";a:1:{s:2:"en";s:27:"Description of test service";}s:16:"OrganizationName";a:1:{s:2:"en";s:17:"Test organization";}s:8:"entityid";s:114:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp";s:14:"metadata-index";s:114:"https://pc-mivancic.srce.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp";s:12:"metadata-set";s:16:"saml20-sp-remote";}', + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES => 'a:5:{s:33:"urn:oid:0.9.2342.19200300.100.1.1";a:1:{i:0;s:11:"student-uid";}s:32:"urn:oid:1.3.6.1.4.1.5923.1.1.1.1";a:2:{i:0;s:6:"member";i:1;s:7:"student";}s:15:"urn:oid:2.5.4.4";a:1:{i:0;s:10:"student-sn";}s:19:"hrEduPersonUniqueID";a:1:{i:0;s:19:"student@example.org";}s:23:"hrEduPersonPersistentID";a:1:{i:0;s:13:"student123abc";}}', + ]; +} diff --git a/tests/src/Constants/SerializedJob.php b/tests/src/Constants/SerializedJob.php new file mode 100644 index 0000000000000000000000000000000000000000..703c055d01c3264f3659957d3a30f23a0b544bef --- /dev/null +++ b/tests/src/Constants/SerializedJob.php @@ -0,0 +1,12 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Constants; + +use SimpleSAML\Module\accounting\Entities\Authentication\Event\Job; + +class SerializedJob +{ + public const EVENT = [ + Job::class => 'TODO' + ]; +} diff --git a/tests/src/Constants/StateArrays.php b/tests/src/Constants/StateArrays.php new file mode 100644 index 0000000000000000000000000000000000000000..7ad8ba490227e4f69f3fe0d638197300a9e7008d --- /dev/null +++ b/tests/src/Constants/StateArrays.php @@ -0,0 +1,170 @@ +<?php +// phpcs:ignoreFile + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Constants; + +final class StateArrays +{ + public const FULL = [ + 'Responder' => [0 => '\\SimpleSAML\\Module\\saml\\IdP\\SAML2', 1 => 'sendResponse',], + '\\SimpleSAML\\Auth\\State.exceptionFunc' => [ + 0 => '\\SimpleSAML\\Module\\saml\\IdP\\SAML2', + 1 => 'handleAuthError', + ], + '\\SimpleSAML\\Auth\\State.restartURL' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/SSOService.php?spentityid=https%3A%2F%2Fpc-example.org.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fsaml%2Fsp%2Fmetadata.php%2Fdefault-sp&RelayState=https%3A%2F%2Flocalhost.someone.from.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fadmin%2Ftest%2Fdefault-sp&cookieTime=1660912195', + 'SPMetadata' => [ + 'SingleLogoutService' => [ + 0 => [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp', + ], + ], + 'AssertionConsumerService' => [ + 0 => [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp', + 'index' => 0, + ], + 1 => [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', + 'Location' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp', + 'index' => 1, + ], + ], + 'contacts' => [ + 0 => [ + 'emailAddress' => 'example@org.hr', + 'givenName' => 'Marko Ivančić', + 'contactType' => 'technical', + ], + ], + 'entityid' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', + 'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', + 'metadata-set' => 'saml20-sp-remote', + ], + 'saml:RelayState' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp', + 'saml:RequestId' => null, + 'saml:IDPList' => [], + 'saml:ProxyCount' => null, + 'saml:RequesterID' => null, + 'ForceAuthn' => false, + 'isPassive' => false, + 'saml:ConsumerURL' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp', + 'saml:Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'saml:NameIDFormat' => null, + 'saml:AllowCreate' => true, + 'saml:Extensions' => null, + 'saml:AuthnRequestReceivedAt' => 1660912195.505402, + 'saml:RequestedAuthnContext' => null, + 'core:IdP' => 'saml2:https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php', + 'core:SP' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', + 'IdPMetadata' => [ + 'host' => 'localhost.someone.from.hr', + 'privatekey' => 'key.pem', + 'certificate' => 'cert.pem', + 'auth' => 'example-userpass', + 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', + 'authproc' => [100 => ['class' => 'core:AttributeMap', 0 => 'name2oid',],], + 'entityid' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php', + 'metadata-index' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php', + 'metadata-set' => 'saml20-idp-hosted', + ], + 'ReturnCallback' => [0 => '\\SimpleSAML\\IdP', 1 => 'postAuth',], + 'Attributes' => [ + 'hrEduPersonUniqueID' => [0 => 'testuser@primjer.hr',], + 'urn:oid:0.9.2342.19200300.100.1.1' => [0 => 'testuser',], + 'urn:oid:2.5.4.3' => [0 => 'TestName TestSurname',], + 'urn:oid:2.5.4.4' => [0 => 'TestSurname', 1 => 'TestSurname2',], + 'urn:oid:2.5.4.42' => [0 => 'TestName',], + 'urn:oid:0.9.2342.19200300.100.1.3' => [0 => 'testusermail@primjer.hr', 1 => 'testusermail2@primjer.hr',], + 'urn:oid:2.5.4.20' => [0 => '123',], + 'hrEduPersonExtensionNumber' => [0 => '123',], + 'urn:oid:0.9.2342.19200300.100.1.41' => [0 => '123',], + 'urn:oid:2.5.4.23' => [0 => '123',], + 'hrEduPersonUniqueNumber' => [0 => 'LOCAL_NO: 100 ', 1 => 'OIB: 00861590360',], + 'hrEduPersonOIB' => [0 => '00861590360',], + 'hrEduPersonDateOfBirth' => [0 => '20200101',], + 'hrEduPersonGender' => [0 => '9',], + 'urn:oid:0.9.2342.19200300.100.1.60' => [0 => '987654321',], + 'urn:oid:2.5.4.36' => [0 => 'cert-value',], + 'urn:oid:1.3.6.1.4.1.250.1.57' => [0 => 'http://www.org.hr/~ivek Home Page',], + 'hrEduPersonProfessionalStatus' => [0 => 'VSS',], + 'hrEduPersonAcademicStatus' => [0 => 'redoviti profesor',], + 'hrEduPersonScienceArea' => [0 => 'sociologija',], + 'hrEduPersonAffiliation' => [0 => 'djelatnik', 1 => 'student',], + 'hrEduPersonPrimaryAffiliation' => [0 => 'djelatnik',], + 'hrEduPersonStudentCategory' => [0 => 'redoviti student:diplomski sveučilišni studij',], + 'hrEduPersonExpireDate' => [0 => '20211231',], + 'hrEduPersonTitle' => [0 => 'rektor',], + 'hrEduPersonRole' => [0 => 'ICT koordinator', 1 => 'ISVU koordinator',], + 'hrEduPersonStaffCategory' => [0 => 'nastavno osoblje', 1 => 'istraživači',], + 'hrEduPersonGroupMember' => [0 => 'grupa 1', 1 => 'grupa 2',], + 'urn:oid:2.5.4.10' => [0 => 'Testna ustanova',], + 'hrEduPersonHomeOrg' => [0 => 'primjer.hr',], + 'urn:oid:2.5.4.11' => [0 => 'Testna org jedinica',], + 'urn:oid:0.9.2342.19200300.100.1.6' => [0 => '123',], + 'urn:oid:2.5.4.16' => [0 => 'Testna ustanova, Baštijanova 52A, HR-10000 Zagreb',], + 'urn:oid:2.5.4.7' => [0 => 'Zagreb',], + 'urn:oid:2.5.4.17' => [0 => 'HR-10000',], + 'urn:oid:2.5.4.9' => [0 => 'Baštijanova 52A',], + 'urn:oid:0.9.2342.19200300.100.1.39' => [0 => 'Testna adresa 52A, HR-10000, Zagreb',], + 'urn:oid:0.9.2342.19200300.100.1.20' => [0 => '123',], + 'hrEduPersonCommURI' => [0 => '123',], + 'hrEduPersonPrivacy' => [0 => 'street', 1 => 'postalCode',], + 'hrEduPersonPersistentID' => [0 => 'da4294fb4e5746d57ab6ad88d2daf275',], + 'urn:oid:2.16.840.1.113730.3.1.241' => [0 => 'testname123',], + 'urn:oid:1.3.6.1.4.1.25178.1.2.12' => [0 => 'skype: pepe.perez',], + 'hrEduPersonCardNum' => [0 => '123',], + 'updatedAt' => [0 => '123456789',], + 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' => [0 => 'urn:example:oidc:manage:client',], + ], + 'Authority' => 'example-userpass', + 'AuthnInstant' => 1660911943, + 'Expire' => 1660940743, + 'ReturnCall' => [0 => '\\SimpleSAML\\IdP', 1 => 'postAuthProc',], + 'Destination' => [ + 'SingleLogoutService' => [ + 0 => [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp', + ], + ], + 'AssertionConsumerService' => [ + 0 => [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp', + 'index' => 0, + ], + 1 => [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', + 'Location' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp', + 'index' => 1, + ], + ], + 'contacts' => [ + 0 => [ + 'emailAddress' => 'example@org.hr', + 'givenName' => 'Marko Ivančić', + 'contactType' => 'technical', + ], + ], + 'entityid' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', + 'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', + 'metadata-set' => 'saml20-sp-remote', + ], + 'Source' => [ + 'host' => 'localhost.someone.from.hr', + 'privatekey' => 'key.pem', + 'certificate' => 'cert.pem', + 'auth' => 'example-userpass', + 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', + 'authproc' => [100 => ['class' => 'core:AttributeMap', 0 => 'name2oid',],], + 'entityid' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php', + 'metadata-index' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php', + 'metadata-set' => 'saml20-idp-hosted', + ], + '\\SimpleSAML\\Auth\\ProcessingChain.filters' => [], + ]; +} diff --git a/tests/src/Entities/Activity/BagTest.php b/tests/src/Entities/Activity/BagTest.php new file mode 100644 index 0000000000000000000000000000000000000000..023dcf5dcea25d8712e996feb9fe69828e5ce77f --- /dev/null +++ b/tests/src/Entities/Activity/BagTest.php @@ -0,0 +1,26 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities\Activity; + +use SimpleSAML\Module\accounting\Entities\Activity; +use SimpleSAML\Module\accounting\Entities\Activity\Bag; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Activity\Bag + */ +class BagTest extends TestCase +{ + public function testCanAddActivity(): void + { + $activityStub = $this->createStub(Activity::class); + $bag = new Bag(); + + $this->assertEmpty($bag->getAll()); + + /** @psalm-suppress InvalidArgument */ + $bag->add($activityStub); + + $this->assertNotEmpty($bag->getAll()); + } +} diff --git a/tests/src/Entities/ActivityTest.php b/tests/src/Entities/ActivityTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2d647655b03313da163d6b6a60e106b8afcd64b3 --- /dev/null +++ b/tests/src/Entities/ActivityTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\Activity; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\ServiceProvider; +use SimpleSAML\Module\accounting\Entities\User; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Activity + */ +class ActivityTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub|ServiceProvider|ServiceProvider&\PHPUnit\Framework\MockObject\Stub + */ + protected $serviceProviderStub; + /** + * @var \PHPUnit\Framework\MockObject\Stub|User|User&\PHPUnit\Framework\MockObject\Stub + */ + protected $userStub; + protected \DateTimeImmutable $happenedAt; + + public function setUp(): void + { + $this->serviceProviderStub = $this->createStub(ServiceProvider::class); + $this->userStub = $this->createStub(User::class); + $this->happenedAt = new \DateTimeImmutable(); + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $activity = new Activity( + $this->serviceProviderStub, + $this->userStub, + $this->happenedAt + ); + + $this->assertSame($this->serviceProviderStub, $activity->getServiceProvider()); + $this->assertSame($this->userStub, $activity->getUser()); + $this->assertSame($this->happenedAt, $activity->getHappenedAt()); + } +} diff --git a/tests/src/Entities/AuthenticationEvent/JobTest.php b/tests/src/Entities/Authentication/Event/JobTest.php similarity index 56% rename from tests/src/Entities/AuthenticationEvent/JobTest.php rename to tests/src/Entities/Authentication/Event/JobTest.php index cbe5e8bf5d3da071d54d62a1af4d3ed2abeeb22a..24551ef3409badd98cd82f8387a6f3c8bc5b3704 100644 --- a/tests/src/Entities/AuthenticationEvent/JobTest.php +++ b/tests/src/Entities/Authentication/Event/JobTest.php @@ -1,25 +1,29 @@ <?php -namespace SimpleSAML\Test\Module\accounting\Entities\AuthenticationEvent; +namespace SimpleSAML\Test\Module\accounting\Entities\Authentication\Event; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job; use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\Event\Job; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; /** - * @covers \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job + * @covers \SimpleSAML\Module\accounting\Entities\Authentication\Event\Job * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper */ class JobTest extends TestCase { public function testCanCreateInstanceWithAuthenticationEventEntity(): void { - $job = new Job(new AuthenticationEvent(['sample' => 'state'])); + $job = new Job(new Event(new State(StateArrays::FULL))); - $this->assertInstanceOf(AuthenticationEvent::class, $job->getPayload()); + $this->assertInstanceOf(Event::class, $job->getPayload()); } public function testPayloadMustBeAuthenticationEventOrThrow(): void @@ -34,7 +38,7 @@ class JobTest extends TestCase public function testCanGetProperType(): void { - $job = new Job(new AuthenticationEvent(['sample' => 'state'])); + $job = new Job(new Event(new State(StateArrays::FULL))); $this->assertSame(Job::class, $job->getType()); } diff --git a/tests/src/Entities/Authentication/EventTest.php b/tests/src/Entities/Authentication/EventTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0141b2d32d967c7271d3f917457d99d5e426c3cf --- /dev/null +++ b/tests/src/Entities/Authentication/EventTest.php @@ -0,0 +1,31 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities\Authentication; + +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper + */ +class EventTest extends TestCase +{ + public function testCanGetState(): void + { + $dateTime = new \DateTimeImmutable(); + $authenticationEvent = new Event(new State(StateArrays::FULL), $dateTime); + + $this->assertInstanceOf(State::class, $authenticationEvent->getState()); + + $this->assertSame( + StateArrays::FULL['Source']['entityid'], + $authenticationEvent->getState()->getIdentityProviderEntityId() + ); + + $this->assertEquals($dateTime, $authenticationEvent->getHappenedAt()); + } +} diff --git a/tests/src/Entities/Authentication/StateTest.php b/tests/src/Entities/Authentication/StateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..32ac9e230ae03527bd3026cf7993d2cbbf8916bf --- /dev/null +++ b/tests/src/Entities/Authentication/StateTest.php @@ -0,0 +1,187 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Entities\Authentication; + +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper + */ +class StateTest extends TestCase +{ + public function testCanInitializeValidState(): void + { + $state = new State(StateArrays::FULL); + + $this->assertSame($state->getIdentityProviderEntityId(), StateArrays::FULL['Source']['entityid']); + } + + public function testCanResolveIdpEntityId(): void + { + $stateArray = StateArrays::FULL; + $state = new State($stateArray); + $this->assertSame($state->getIdentityProviderEntityId(), StateArrays::FULL['IdPMetadata']['entityid']); + + $this->expectException(UnexpectedValueException::class); + unset($stateArray['IdPMetadata']['entityid']); + new State($stateArray); + } + + public function testCanResolveSpEntityId(): void + { + $stateArray = StateArrays::FULL; + $state = new State($stateArray); + $this->assertSame($state->getServiceProviderEntityId(), StateArrays::FULL['SPMetadata']['entityid']); + + $this->expectException(UnexpectedValueException::class); + unset($stateArray['SPMetadata']['entityid']); + new State($stateArray); + } + + public function testCanResolveAttributes(): void + { + $state = new State(StateArrays::FULL); + $this->assertSame($state->getAttributes(), StateArrays::FULL['Attributes']); + } + + public function testCanResolveAccountedClientIpAddress(): void + { + $stateArray = StateArrays::FULL; + + $state = new State($stateArray); + $this->assertNull($state->getClientIpAddress()); + + $sampleIp = '123.123.123.123'; + $stateArray[State::KEY_ACCOUNTING][State::ACCOUNTING_KEY_CLIENT_IP_ADDRESS] = $sampleIp; + + $state = new State($stateArray); + $this->assertSame($sampleIp, $state->getClientIpAddress()); + } + + public function testReturnsNullIfAuthnInstantNotPresent(): void + { + $stateArray = StateArrays::FULL; + + unset($stateArray['AuthnInstant']); + + $state = new State($stateArray); + + $this->assertNull($state->getAuthenticationInstant()); + } + + public function testThrowsOnMissingSourceEntityId(): void + { + $this->expectException(UnexpectedValueException::class); + + $stateArray = StateArrays::FULL; + + unset($stateArray['Source'], $stateArray['IdPMetadata']); + + /** @psalm-suppress UnusedMethodCall */ + (new State($stateArray)); + } + + public function testUseSpMetadataForEntityIdIfDestinationNotAvailable(): void + { + $stateArray = StateArrays::FULL; + + unset($stateArray['Destination']); + + $state = new State($stateArray); + + $this->assertSame($state->getServiceProviderEntityId(), StateArrays::FULL['SPMetadata']['entityid']); + } + + public function testThrowsOnMissingDestinationEntityId(): void + { + $this->expectException(UnexpectedValueException::class); + + $stateArray = StateArrays::FULL; + + unset($stateArray['Destination'], $stateArray['SPMetadata']); + + (new State($stateArray)); + } + + public function testThrowsOnInvalidAuthnInstantValue(): void + { + $this->expectException(UnexpectedValueException::class); + + $stateArray = StateArrays::FULL; + $stateArray['AuthnInstant'] = 'invalid'; + + new State($stateArray); + } + + public function testThrowsOnMissingAttributes(): void + { + $this->expectException(UnexpectedValueException::class); + + $stateArray = StateArrays::FULL; + + unset($stateArray['Attributes']); + + /** @psalm-suppress UnusedMethodCall */ + (new State($stateArray)); + } + + public function testCanGetAttributeValue(): void + { + $state = new State(StateArrays::FULL); + + $this->assertSame( + StateArrays::FULL['Attributes']['hrEduPersonUniqueID'][0], + $state->getAttributeValue('hrEduPersonUniqueID') + ); + + $this->assertNull($state->getAttributeValue('non-existent')); + } + + public function testCanResolveIdpMetadataArray(): void + { + // Metadata from 'IdPMetadata' + $sampleState = StateArrays::FULL; + $state = new State($sampleState); + $this->assertEquals($sampleState['IdPMetadata'], $state->getIdentityProviderMetadata()); + + // Fallback metadata from 'Source' + unset($sampleState['IdPMetadata']); + $state = new State($sampleState); + $this->assertEquals($sampleState['Source'], $state->getIdentityProviderMetadata()); + + // Throws on no IdP metadata + $this->expectException(UnexpectedValueException::class); + unset($sampleState['Source']); + new State($sampleState); + } + + public function testCanResolveSpMetadataArray(): void + { + // Metadata from 'IdPMetadata' + $sampleState = StateArrays::FULL; + $state = new State($sampleState); + $this->assertEquals($sampleState['SPMetadata'], $state->getServiceProviderMetadata()); + + // Fallback metadata from 'Destination' + unset($sampleState['SPMetadata']); + $state = new State($sampleState); + $this->assertEquals($sampleState['Destination'], $state->getServiceProviderMetadata()); + + // Throws on no SP metadata + $this->expectException(UnexpectedValueException::class); + unset($sampleState['Destination']); + new State($sampleState); + } + + public function testCanGetCreatedAt(): void + { + $state = new State(StateArrays::FULL); + $this->assertInstanceOf(\DateTimeImmutable::class, $state->getCreatedAt()); + } +} diff --git a/tests/src/Entities/AuthenticationEventTest.php b/tests/src/Entities/AuthenticationEventTest.php deleted file mode 100644 index bf4ceb2101c946b9abea1fc5bbe8f662d5d88aa3..0000000000000000000000000000000000000000 --- a/tests/src/Entities/AuthenticationEventTest.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php - -namespace SimpleSAML\Test\Module\accounting\Entities; - -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; -use PHPUnit\Framework\TestCase; - -/** - * @covers \SimpleSAML\Module\accounting\Entities\AuthenticationEvent - */ -class AuthenticationEventTest extends TestCase -{ - protected array $state = [ - 'sample' => 'value', - ]; - - - public function testCanGetState(): void - { - $authenticationEvent = new AuthenticationEvent($this->state); - - $this->assertArrayHasKey('sample', $authenticationEvent->getState()); - } -} diff --git a/tests/src/Entities/Bases/AbstractJobTest.php b/tests/src/Entities/Bases/AbstractJobTest.php index 88d265716e59a622069ab65aa2ef27488b51a7b9..22725b8079bf41a9070af20b1b4922af94b1270f 100644 --- a/tests/src/Entities/Bases/AbstractJobTest.php +++ b/tests/src/Entities/Bases/AbstractJobTest.php @@ -2,16 +2,14 @@ namespace SimpleSAML\Test\Module\accounting\Entities\Bases; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; /** * @covers \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event\Job + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event */ class AbstractJobTest extends TestCase { diff --git a/tests/src/Entities/Bases/AbstractProviderTest.php b/tests/src/Entities/Bases/AbstractProviderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ea71711d5822d35a8f173c125ac34f5c1928c4fa --- /dev/null +++ b/tests/src/Entities/Bases/AbstractProviderTest.php @@ -0,0 +1,66 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities\Bases; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\IdentityProvider; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + * @uses \SimpleSAML\Module\accounting\Entities\IdentityProvider + */ +class AbstractProviderTest extends TestCase +{ + /** + * @var array + */ + protected array $metadata; + + public function setUp(): void + { + $this->metadata = [ + AbstractProvider::METADATA_KEY_ENTITY_ID => 'http//example.org/idp', + AbstractProvider::METADATA_KEY_NAME => [ + 'en' => 'Test', + ], + ]; + } + + /** + * @psalm-suppress MixedArrayAccess + */ + public function testCanCreateInstance(): void + { + $identityProvider = new IdentityProvider($this->metadata); + + $this->assertSame($this->metadata, $identityProvider->getMetadata()); + $this->assertSame( + $this->metadata[AbstractProvider::METADATA_KEY_ENTITY_ID], + $identityProvider->getEntityId() + ); + $this->assertSame( + $this->metadata[AbstractProvider::METADATA_KEY_NAME]['en'], + $identityProvider->getName() + ); + } + + public function testReturnsNullIfNameNotAvailable(): void + { + $metadata = $this->metadata; + unset($metadata[AbstractProvider::METADATA_KEY_NAME]); + + $identityProvider = new IdentityProvider($metadata); + $this->assertNull($identityProvider->getName()); + } + + public function testThrowsIfEntityIdNotAvailable(): void + { + $metadata = $this->metadata; + unset($metadata[AbstractProvider::METADATA_KEY_ENTITY_ID]); + + $this->expectException(UnexpectedValueException::class); + new IdentityProvider($metadata); + } +} diff --git a/tests/src/Entities/ConnectedServiceProvider/BagTest.php b/tests/src/Entities/ConnectedServiceProvider/BagTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3d62a773c429d5a84e8df4de9d4bb817e5ab712f --- /dev/null +++ b/tests/src/Entities/ConnectedServiceProvider/BagTest.php @@ -0,0 +1,23 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities\ConnectedServiceProvider; + +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider\Bag; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider\Bag + */ +class BagTest extends TestCase +{ + public function testCanAddConnectedService(): void + { + $connectedServiceProvider = $this->createStub(ConnectedServiceProvider::class); + $bag = new Bag(); + + $this->assertEmpty($bag->getAll()); + $bag->addOrReplace($connectedServiceProvider); + $this->assertNotEmpty($bag->getAll()); + } +} diff --git a/tests/src/Entities/ConnectedServiceProviderTest.php b/tests/src/Entities/ConnectedServiceProviderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..186fb2e9a71b22c51e71de0be6ef6bb1b11bf406 --- /dev/null +++ b/tests/src/Entities/ConnectedServiceProviderTest.php @@ -0,0 +1,51 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\ServiceProvider; +use SimpleSAML\Module\accounting\Entities\User; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider + */ +class ConnectedServiceProviderTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub|ServiceProvider|ServiceProvider&\PHPUnit\Framework\MockObject\Stub + */ + protected $serviceProviderStub; + /** + * @var \PHPUnit\Framework\MockObject\Stub|User|User&\PHPUnit\Framework\MockObject\Stub + */ + protected $userStub; + protected \DateTimeImmutable $dateTime; + protected int $numberOfAuthentications; + + public function setUp(): void + { + $this->serviceProviderStub = $this->createStub(ServiceProvider::class); + $this->userStub = $this->createStub(User::class); + $this->dateTime = new \DateTimeImmutable(); + $this->numberOfAuthentications = 1; + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $connectedServiceProvider = new ConnectedServiceProvider( + $this->serviceProviderStub, + $this->numberOfAuthentications, + $this->dateTime, + $this->dateTime, + $this->userStub + ); + + $this->assertSame($this->serviceProviderStub, $connectedServiceProvider->getServiceProvider()); + $this->assertSame($this->numberOfAuthentications, $connectedServiceProvider->getNumberOfAuthentications()); + $this->assertSame($this->dateTime, $connectedServiceProvider->getFirstAuthenticationAt()); + $this->assertSame($this->dateTime, $connectedServiceProvider->getLastAuthenticationAt()); + $this->assertSame($this->userStub, $connectedServiceProvider->getUser()); + } +} diff --git a/tests/src/Entities/IdentityProviderTest.php b/tests/src/Entities/IdentityProviderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..401c926b2162f5901a991687dc4b2ba7094258d5 --- /dev/null +++ b/tests/src/Entities/IdentityProviderTest.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; +use SimpleSAML\Module\accounting\Entities\IdentityProvider; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\IdentityProvider + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + */ +class IdentityProviderTest extends TestCase +{ + /** + * @var string[] + */ + protected array $metadata; + + public function setUp(): void + { + $this->metadata = [ + AbstractProvider::METADATA_KEY_ENTITY_ID => 'http//example.org/idp' + ]; + } + + public function testCanCreateInstance(): void + { + $identityProvider = new IdentityProvider($this->metadata); + $this->assertSame($this->metadata, $identityProvider->getMetadata()); + } +} diff --git a/tests/src/Entities/ServiceProviderTest.php b/tests/src/Entities/ServiceProviderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3c959231ab46acd593973adc08f81297e8aa2011 --- /dev/null +++ b/tests/src/Entities/ServiceProviderTest.php @@ -0,0 +1,31 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\ServiceProvider; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\ServiceProvider + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + */ +class ServiceProviderTest extends TestCase +{ + /** + * @var string[] + */ + protected array $metadata; + + public function setUp(): void + { + $this->metadata = [ + 'entityid' => 'http//example.org' + ]; + } + + public function testCanCreateInstance(): void + { + $serviceProvider = new ServiceProvider($this->metadata); + $this->assertSame($this->metadata, $serviceProvider->getMetadata()); + } +} diff --git a/tests/src/Entities/UserTest.php b/tests/src/Entities/UserTest.php new file mode 100644 index 0000000000000000000000000000000000000000..21199404ffe97212b410c4138b828daa4f2645fc --- /dev/null +++ b/tests/src/Entities/UserTest.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\User; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\User + */ +class UserTest extends TestCase +{ + /** + * @var string[][] + */ + protected array $attributes; + + protected function setUp(): void + { + $this->attributes = [ + 'uid' => ['test'], + ]; + } + + public function testCanCreateInstance(): void + { + $user = new User($this->attributes); + $this->assertSame($this->attributes, $user->getAttributes()); + } +} diff --git a/tests/src/Helpers/ArrayHelperTest.php b/tests/src/Helpers/ArrayHelperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2e2d77912233193e23d6d38d327d948fb93901a1 --- /dev/null +++ b/tests/src/Helpers/ArrayHelperTest.php @@ -0,0 +1,31 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Helpers; + +use SimpleSAML\Module\accounting\Helpers\ArrayHelper; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Helpers\ArrayHelper + */ +class ArrayHelperTest extends TestCase +{ + public function testCanRecursivelySortByKey(): void + { + $unsorted = [ + 'b' => [1 => 1, 0 => 0], + 'a' => [1 => 1, 0 => 0], + ]; + + $sorted = [ + 'a' => [0 => 0, 1 => 1], + 'b' => [0 => 0, 1 => 1], + ]; + + $this->assertNotSame($unsorted, $sorted); + + ArrayHelper::recursivelySortByKey($unsorted); + + $this->assertSame($unsorted, $sorted); + } +} diff --git a/tests/src/Helpers/AttributesHelperTest.php b/tests/src/Helpers/AttributesHelperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9d21c87a33ec8bd68ae96e513fcb8af641db0115 --- /dev/null +++ b/tests/src/Helpers/AttributesHelperTest.php @@ -0,0 +1,47 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Helpers; + +use SimpleSAML\Module\accounting\Helpers\AttributesHelper; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +/** + * @covers \SimpleSAML\Module\accounting\Helpers\AttributesHelper + * @uses \SimpleSAML\Module\accounting\ModuleConfiguration + */ +class AttributesHelperTest extends TestCase +{ + /** + * @var string $sspBaseDir Simulated SSP base directory. + */ + protected string $sspBaseDir; + + /** + * @var string[] + */ + protected array $mapFiles; + + protected function setUp(): void + { + $this->sspBaseDir = (new ModuleConfiguration())->getModuleRootDirectory() . DIRECTORY_SEPARATOR . 'tests' . + DIRECTORY_SEPARATOR; + + $this->mapFiles = ['test.php', 'test2.php']; + } + + public function testCanLoadAttributeMaps(): void + { + $fullAttributeMap = AttributesHelper::getMergedAttributeMapForFiles($this->sspBaseDir, $this->mapFiles); + + $this->assertArrayHasKey('mobile', $fullAttributeMap); + $this->assertArrayHasKey('phone', $fullAttributeMap); + } + + public function testIgnoresNonExistentMaps(): void + { + $fullAttributeMap = AttributesHelper::getMergedAttributeMapForFiles($this->sspBaseDir, ['invalid.php']); + + $this->assertEmpty($fullAttributeMap); + } +} diff --git a/tests/src/Helpers/HashHelperTest.php b/tests/src/Helpers/HashHelperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6c2bf4325f014ff34accc1ef2e20bcd004a54629 --- /dev/null +++ b/tests/src/Helpers/HashHelperTest.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Helpers; + +use SimpleSAML\Module\accounting\Helpers\HashHelper; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Helpers\HashHelper + * @uses \SimpleSAML\Module\accounting\Helpers\ArrayHelper + */ +class HashHelperTest extends TestCase +{ + protected string $data; + protected string $dataSha256; + /** + * @var \int[][] + */ + protected array $unsortedArrayData; + protected string $unsortedArraySha256; + /** + * @var \int[][] + */ + protected array $sortedArrayData; + protected string $sortedArrayDataSha256; + + protected function setUp(): void + { + $this->data = 'test'; + $this->dataSha256 = '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'; + $this->unsortedArrayData = ['b' => [1 => 1, 0 => 0,], 'a' => [1 => 1, 0 => 0,],]; + $this->unsortedArraySha256 = 'c467191ddddcaa5a6cc3bac28c1fd0557eb9390dbb079195a2ae70c49ce62da7'; + $this->sortedArrayData = ['a' => [0 => 0, 1 => 1,], 'b' => [0 => 0, 1 => 1,],]; + $this->sortedArrayDataSha256 = 'c467191ddddcaa5a6cc3bac28c1fd0557eb9390dbb079195a2ae70c49ce62da7'; + } + + public function testCanGetSha256ForString(): void + { + $this->assertSame($this->dataSha256, HashHelper::getSha256($this->data)); + } + + public function testCanGetSha256ForArray(): void + { + // Arrays are sorted before the hash is calculated, so the value must be the same. + $this->assertSame($this->unsortedArraySha256, $this->sortedArrayDataSha256); + $this->assertSame($this->unsortedArraySha256, HashHelper::getSha256ForArray($this->unsortedArrayData)); + $this->assertSame($this->sortedArrayDataSha256, HashHelper::getSha256ForArray($this->sortedArrayData)); + } +} diff --git a/tests/src/Helpers/InstanceBuilderUsingModuleConfigurationHelperTest.php b/tests/src/Helpers/InstanceBuilderUsingModuleConfigurationHelperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a1caa56d4ba6a56f6601956407ba388536ba14d3 --- /dev/null +++ b/tests/src/Helpers/InstanceBuilderUsingModuleConfigurationHelperTest.php @@ -0,0 +1,64 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Helpers; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\Exception; +use SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +/** + * @covers \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper + */ +class InstanceBuilderUsingModuleConfigurationHelperTest extends TestCase +{ + protected BuildableUsingModuleConfigurationInterface $stub; + /** @var class-string */ + protected string $stubClass; + protected \PHPUnit\Framework\MockObject\Stub $moduleConfigurationstub; + protected \PHPUnit\Framework\MockObject\Stub $loggerStub; + + protected function setUp(): void + { + $this->stub = new class () implements BuildableUsingModuleConfigurationInterface { + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger + ): BuildableUsingModuleConfigurationInterface { + return new self(); + } + }; + + $this->stubClass = get_class($this->stub); + + $this->moduleConfigurationstub = $this->createStub(ModuleConfiguration::class); + $this->loggerStub = $this->createStub(LoggerInterface::class); + } + + public function testCanBuildClassInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf( + BuildableUsingModuleConfigurationInterface::class, + InstanceBuilderUsingModuleConfigurationHelper::build( + $this->stubClass, + $this->moduleConfigurationstub, + $this->loggerStub + ) + ); + } + + public function testThrowsForInvalidClass(): void + { + $this->expectException(Exception::class); + + /** @psalm-suppress InvalidArgument */ + InstanceBuilderUsingModuleConfigurationHelper::build( + ModuleConfiguration::class, // Sample class which is not buildable. + $this->moduleConfigurationstub, + $this->loggerStub + ); + } +} diff --git a/tests/src/Helpers/NetworkHelperTest.php b/tests/src/Helpers/NetworkHelperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d4c9abe03a73b2e5649275dc2142c684d5332fc4 --- /dev/null +++ b/tests/src/Helpers/NetworkHelperTest.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Helpers; + +use SimpleSAML\Module\accounting\Helpers\NetworkHelper; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Helpers\NetworkHelper + */ +class NetworkHelperTest extends TestCase +{ + protected string $ipAddress; + + protected function setUp(): void + { + $this->ipAddress = '123.123.123.123'; + } + + public function testCanGetIpFromParameter(): void + { + $this->assertSame($this->ipAddress, NetworkHelper::resolveClientIpAddress($this->ipAddress)); + } + + public function testReturnsNullForInvalidIp(): void + { + $this->assertNull(NetworkHelper::resolveClientIpAddress('invalid')); + } + + public function testReturnsNullForNonExistentIp(): void + { + $this->assertNull(NetworkHelper::resolveClientIpAddress()); + } + + /** + * @backupGlobals enabled + */ + public function testCanResolveIpAddress(): void + { + global $_SERVER; + + $_SERVER['REMOTE_ADDR'] = $this->ipAddress; + + $this->assertSame($this->ipAddress, NetworkHelper::resolveClientIpAddress()); + } +} diff --git a/tests/src/ModuleConfigurationTest.php b/tests/src/ModuleConfigurationTest.php index 9895cb8421003962067858c67c200f7bd152c66f..80821ec553e3044be13026a758575a92605559ae 100644 --- a/tests/src/ModuleConfigurationTest.php +++ b/tests/src/ModuleConfigurationTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Test\Module\accounting; use PHPUnit\Framework\TestCase; @@ -7,6 +9,7 @@ use SimpleSAML\Configuration; use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Stores; +use SimpleSAML\Module\accounting\Trackers; /** * @covers \SimpleSAML\Module\accounting\ModuleConfiguration @@ -36,14 +39,80 @@ class ModuleConfigurationTest extends TestCase public function testCanGetValidOption(): void { - $this->assertIsString($this->moduleConfiguration->get(ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE)); + $this->assertIsString($this->moduleConfiguration->get(ModuleConfiguration::OPTION_USER_ID_ATTRIBUTE_NAME)); + } + + public function testCanGetUserIdAttributeName(): void + { + $this->assertIsString($this->moduleConfiguration->getUserIdAttributeName()); + } + + public function testCanGetDefaultAuthenticationSource(): void + { + $this->assertIsString($this->moduleConfiguration->getDefaultAuthenticationSource()); + } + + public function testCanGetJobsStoreClass(): void + { + $this->assertTrue( + is_subclass_of($this->moduleConfiguration->getJobsStoreClass(), Stores\Interfaces\JobsStoreInterface::class) + ); + } + + public function testThrowsForInvalidConfig(): void + { + $this->expectException(InvalidConfigurationException::class); + + new ModuleConfiguration('invalid_module_accounting.php'); + } + + public function testThrowsForInvalidJobsStore(): void + { + $this->expectException(InvalidConfigurationException::class); + + new ModuleConfiguration('invalid_async_module_accounting.php'); } public function testProperConnectionKeyIsReturned(): void { $this->assertSame( - 'doctrine_dbal_pdo_mysql', - $this->moduleConfiguration->getStoreConnection(Stores\Jobs\DoctrineDbal\JobsStore::class) + 'doctrine_dbal_pdo_sqlite', + $this->moduleConfiguration->getClassConnectionKey(Stores\Jobs\DoctrineDbal\Store::class) + ); + } + + public function testCanGetSlaveConnectionKey(): void + { + $this->assertSame( + 'doctrine_dbal_pdo_sqlite_slave', + $this->moduleConfiguration->getClassConnectionKey( + Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class, + ModuleConfiguration\ConnectionType::SLAVE + ) + ); + } + + public function testThrowsForNonStringAndNonArrayConnectionKey(): void + { + $this->expectException(InvalidConfigurationException::class); + + new ModuleConfiguration('invalid_object_value_module_accounting.php'); + } + + public function testThrowsForNonMasterInArrayConnection(): void + { + $this->expectException(InvalidConfigurationException::class); + + new ModuleConfiguration('invalid_array_value_module_accounting.php'); + } + + public function testThrowsForInvalidConnectiontype(): void + { + $this->expectException(InvalidConfigurationException::class); + + $this->moduleConfiguration->getClassConnectionKey( + Stores\Jobs\DoctrineDbal\Store::class, + 'invalid' ); } @@ -51,26 +120,26 @@ class ModuleConfigurationTest extends TestCase { $this->expectException(InvalidConfigurationException::class); - $this->moduleConfiguration->getStoreConnection('invalid'); + $this->moduleConfiguration->getClassConnectionParameters('invalid'); } public function testCanGetDefinedConnections(): void { $this->assertArrayHasKey( - 'doctrine_dbal_pdo_mysql', - $this->moduleConfiguration->getAllStoreConnectionsAndParameters() + 'doctrine_dbal_pdo_sqlite', + $this->moduleConfiguration->getConnectionsAndParameters() ); } - public function testCanGetSettingsForSpecificConnection(): void + public function testCanGetParametersForSpecificConnection(): void { - $this->assertIsArray($this->moduleConfiguration->getStoreConnectionParameters('doctrine_dbal_pdo_mysql')); + $this->assertIsArray($this->moduleConfiguration->getConnectionParameters('doctrine_dbal_pdo_sqlite')); } public function testGettingSettingsForInvalidConnectionThrows(): void { $this->expectException(InvalidConfigurationException::class); - $this->moduleConfiguration->getStoreConnectionParameters('invalid'); + $this->moduleConfiguration->getConnectionParameters('invalid'); } public function testCanGetModuleSourceDirectory(): void diff --git a/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php b/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..99c95c87613f4a9e1147195e2e56fed5967f5709 --- /dev/null +++ b/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Providers\Builders; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\Exception; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Providers\Builders\AuthenticationDataProviderBuilder; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Providers\Builders\AuthenticationDataProviderBuilder + * @uses \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper + * @uses \SimpleSAML\Module\accounting\Stores\Builders\Bases\AbstractStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore + * @uses \SimpleSAML\Module\accounting\Stores\Builders\DataStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store + * @uses \SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker + */ +class AuthenticationDataProviderBuilderTest extends TestCase +{ + protected \PHPUnit\Framework\MockObject\Stub $moduleConfigurationStub; + + protected \PHPUnit\Framework\MockObject\Stub $loggerStub; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $connectionParams = ConnectionParameters::DBAL_SQLITE_MEMORY; + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn($connectionParams); + + $this->loggerStub = $this->createStub(LoggerInterface::class); + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf( + AuthenticationDataProviderBuilder::class, + new AuthenticationDataProviderBuilder($this->moduleConfigurationStub, $this->loggerStub) + ); + } + + public function testCanBuildDataProvider(): void + { + /** @psalm-suppress InvalidArgument */ + $builder = new AuthenticationDataProviderBuilder($this->moduleConfigurationStub, $this->loggerStub); + + $this->assertInstanceOf(Tracker::class, $builder->build(Tracker::class)); + } + + public function testThrowsForInvalidClass(): void + { + $this->expectException(Exception::class); + + /** @psalm-suppress InvalidArgument */ + (new AuthenticationDataProviderBuilder($this->moduleConfigurationStub, $this->loggerStub)) + ->build('invalid'); + } +} diff --git a/tests/src/Services/LoggerServiceTest.php b/tests/src/Services/LoggerServiceTest.php index a1ffe30c1dffb0b8199303755c8fa0d5e2c18fe2..241dfeb4a106a5d05bb64ea84d048dc431dcd7e5 100644 --- a/tests/src/Services/LoggerServiceTest.php +++ b/tests/src/Services/LoggerServiceTest.php @@ -2,17 +2,17 @@ namespace SimpleSAML\Test\Module\accounting\Services; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Services\Logger; use PHPUnit\Framework\TestCase; /** - * @covers \SimpleSAML\Module\accounting\Services\LoggerService + * @covers \SimpleSAML\Module\accounting\Services\Logger */ class LoggerServiceTest extends TestCase { public function testCanCallAllMethods(): void { - $loggerService = new LoggerService(); + $loggerService = new Logger(); $loggerService->stats('test'); $loggerService->debug('test'); diff --git a/tests/src/Stores/Bases/DoctrineDbal/AbstractRawEntityTest.php b/tests/src/Stores/Bases/DoctrineDbal/AbstractRawEntityTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9f8ae4cf8bc0acc63e322a7c73a7875d52932652 --- /dev/null +++ b/tests/src/Stores/Bases/DoctrineDbal/AbstractRawEntityTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Stores\Bases\DoctrineDbal; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Test\Module\accounting\Constants\DateTime; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + */ +class AbstractRawEntityTest extends TestCase +{ + /** + * @var AbstractPlatform|AbstractPlatform&\PHPUnit\Framework\MockObject\Stub|\PHPUnit\Framework\MockObject\Stub + */ + protected $abstractPlatformStub; + /** + * @var string[] + */ + protected array $rawRow; + + protected function setUp(): void + { + $this->abstractPlatformStub = $this->createStub(AbstractPlatform::class); + $this->abstractPlatformStub->method('getDateTimeFormatString') + ->willReturn(DateTime::DEFAULT_FORMAT); + $this->rawRow = ['sample' => 'test']; + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $rawEntityInstance = new class ($this->rawRow, $this->abstractPlatformStub) extends AbstractRawEntity { + protected function validate( + array $rawRow + ): void { + } + }; + $this->assertInstanceOf(AbstractRawEntity::class, $rawEntityInstance); + } + + public function testCanResolveDateTimeImmutable(): void + { + /** @psalm-suppress InvalidArgument */ + $rawEntityInstance = new class ($this->rawRow, $this->abstractPlatformStub) extends AbstractRawEntity { + protected function validate( + array $rawRow + ): void { + $this->resolveDateTimeImmutable('2022-09-21 14:49:20'); + } + }; + + $this->assertInstanceOf(AbstractRawEntity::class, $rawEntityInstance); + } + + public function testThrowsForInvalidDateTime(): void + { + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new class ($this->rawRow, $this->abstractPlatformStub) extends AbstractRawEntity { + protected function validate( + array $rawRow + ): void { + $this->resolveDateTimeImmutable('invalid'); + } + }; + } +} diff --git a/tests/src/Stores/Builders/DataStoreBuilderTest.php b/tests/src/Stores/Builders/DataStoreBuilderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..30b4214b86d8a0425948a2370580e1dd551d7fa1 --- /dev/null +++ b/tests/src/Stores/Builders/DataStoreBuilderTest.php @@ -0,0 +1,52 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Builders; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Builders\DataStoreBuilder; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Builders\DataStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store + * @uses \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore + * @uses \SimpleSAML\Module\accounting\Stores\Builders\Bases\AbstractStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository + */ +class DataStoreBuilderTest extends TestCase +{ + protected \PHPUnit\Framework\MockObject\Stub $moduleConfigurationStub; + protected \PHPUnit\Framework\MockObject\Stub $loggerStub; + protected DataStoreBuilder $dataStoreBuilder; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + + $this->loggerStub = $this->createStub(LoggerInterface::class); + + /** @psalm-suppress InvalidArgument */ + $this->dataStoreBuilder = new DataStoreBuilder($this->moduleConfigurationStub, $this->loggerStub); + } + + public function testCanBuildDataStore(): void + { + $this->assertInstanceOf(Store::class, $this->dataStoreBuilder->build(Store::class)); + } + + public function testThrowsForInvalidDataStoreClass(): void + { + $this->expectException(StoreException::class); + $this->dataStoreBuilder->build(ModuleConfiguration::class); + } +} diff --git a/tests/src/Stores/Builders/JobsStoreBuilderTest.php b/tests/src/Stores/Builders/JobsStoreBuilderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fb212835e96b6a2953d8514c4b1eb6a41edcf508 --- /dev/null +++ b/tests/src/Stores/Builders/JobsStoreBuilderTest.php @@ -0,0 +1,116 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Builders; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Builders\Bases\AbstractStoreBuilder; +use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder; +use SimpleSAML\Module\accounting\Stores\Interfaces\StoreInterface; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Builders\Bases\AbstractStoreBuilder + * @covers \SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore + * @uses \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper + */ +class JobsStoreBuilderTest extends TestCase +{ + protected \PHPUnit\Framework\MockObject\Stub $moduleConfigurationStub; + protected \PHPUnit\Framework\MockObject\Stub $loggerStub; + protected JobsStoreBuilder $jobsStoreBuilder; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->moduleConfigurationStub->method('getJobsStoreClass')->willReturn(Store::class); + + $this->loggerStub = $this->createStub(LoggerInterface::class); + + /** @psalm-suppress InvalidArgument */ + $this->jobsStoreBuilder = new JobsStoreBuilder($this->moduleConfigurationStub, $this->loggerStub); + } + + public function testCanBuildJobsStore(): void + { + $this->assertInstanceOf(Store::class, $this->jobsStoreBuilder->build(Store::class)); + } + + public function testThrowsForInvalidStoreClass(): void + { + $moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + + $invalidStore = new class { + }; + + /** @psalm-suppress InvalidArgument */ + $storeBuilder = new class ($moduleConfigurationStub, $this->loggerStub) extends AbstractStoreBuilder { + public function build(string $class, string $connectionKey = null): StoreInterface + { + return $this->buildGeneric($class, [$connectionKey]); + } + }; + + $this->expectException(StoreException::class); + + /** @psalm-suppress InvalidArgument */ + $storeBuilder->build(get_class($invalidStore)); + } + + public function testThrowsForInvalidJobsStoreClass(): void + { + $moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + + $this->expectException(StoreException::class); + + /** @psalm-suppress InvalidArgument */ + (new JobsStoreBuilder($moduleConfigurationStub, $this->loggerStub))->build('invalid'); + } + + public function testJobsStoreBuilderOnlyReturnsJobsStores(): void + { + $sampleStore = new class implements StoreInterface { + public function needsSetup(): bool + { + return false; + } + + public function runSetup(): void + { + } + + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): StoreInterface { + return new self(); + } + }; + + $moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + $moduleConfigurationStub->method('getJobsStoreClass')->willReturn(get_class($sampleStore)); + + $this->expectException(StoreException::class); + + /** @psalm-suppress InvalidArgument */ + (new JobsStoreBuilder($moduleConfigurationStub, $this->loggerStub))->build(get_class($sampleStore)); + } +} diff --git a/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php b/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php index a9bda1bf9ed49eeb2630b7d6585f2fe7582a397c..1f4f94dd1b7f023d4f941b7fc096aafc8854dc33 100644 --- a/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php +++ b/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php @@ -1,17 +1,20 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Test\Module\accounting\Stores\Connections\Bases; use Doctrine\DBAL\Schema\AbstractSchemaManager; use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Services\Logger; use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; use PHPUnit\Framework\TestCase; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; /** * @covers \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator @@ -19,9 +22,10 @@ use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; * @uses \SimpleSAML\Module\accounting\ModuleConfiguration * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000100CreateJobFailedTable * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases\AbstractCreateJobsTable */ class AbstractMigratorTest extends TestCase { @@ -38,21 +42,12 @@ class AbstractMigratorTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); - // TODO mivanci ostavi samo sqlite verziju -// $this->connection = new Connection([ -// 'dbname' => 'accounting', -// 'user' => 'apps', -// 'password' => 'apps', -// 'host' => '127.0.0.1', -// 'port' => '33306', -// 'driver' => 'pdo_mysql', -// ]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); $this->schemaManager = $this->connection->dbal()->createSchemaManager(); $this->tableName = $this->connection->preparePrefixedTableName(Migrator::TABLE_NAME); - $this->loggerServiceMock = $this->createMock(LoggerService::class); + $this->loggerServiceMock = $this->createMock(Logger::class); // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml). $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php'); @@ -60,7 +55,7 @@ class AbstractMigratorTest extends TestCase public function testCanGatherMigrationClassesFromDirectory(): void { - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $directory = $this->getSampleMigrationsDirectory(); @@ -69,12 +64,12 @@ class AbstractMigratorTest extends TestCase $migrationClasses = $migrator->gatherMigrationClassesFromDirectory($directory, $namespace); - $this->assertTrue(in_array($namespace . '\Version20220601000000CreateJobsTable', $migrationClasses)); + $this->assertTrue(in_array($namespace . '\Version20220601000000CreateJobTable', $migrationClasses)); } public function testCanRunMigrationClasses(): void { - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $migrator->runSetup(); @@ -85,7 +80,7 @@ class AbstractMigratorTest extends TestCase $migrationClasses = $migrator->gatherMigrationClassesFromDirectory($directory, $namespace); - $jobsTableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); + $jobsTableName = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); $this->assertFalse($this->schemaManager->tablesExist($jobsTableName)); @@ -96,7 +91,7 @@ class AbstractMigratorTest extends TestCase public function testCanGatherOnlyMigrationClasses(): void { - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $directory = __DIR__; @@ -119,7 +114,7 @@ class AbstractMigratorTest extends TestCase } }; - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $this->expectException(MigrationException::class); @@ -129,7 +124,7 @@ class AbstractMigratorTest extends TestCase public function testCanGetNonImplementedMigrationClasses(): void { - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $migrator->runSetup(); @@ -140,14 +135,14 @@ class AbstractMigratorTest extends TestCase ); $this->assertTrue(in_array( - JobsStore\Migrations\Version20220601000000CreateJobsTable::class, + Store\Migrations\Version20220601000000CreateJobTable::class, $nonImplementedMigrationClasses )); } public function testCanFindOutIfNonImplementedMigrationClassesExist(): void { - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $migrator->runSetup(); @@ -160,7 +155,7 @@ class AbstractMigratorTest extends TestCase public function testCanRunNonImplementedMigrationClasses(): void { - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $migrator->runSetup(); @@ -179,11 +174,11 @@ class AbstractMigratorTest extends TestCase { return $this->moduleConfiguration->getModuleSourceDirectory() . DIRECTORY_SEPARATOR . 'Stores' . DIRECTORY_SEPARATOR . 'Jobs' . DIRECTORY_SEPARATOR . 'DoctrineDbal' . DIRECTORY_SEPARATOR . - 'JobsStore' . DIRECTORY_SEPARATOR . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + 'Store' . DIRECTORY_SEPARATOR . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; } protected function getSampleNameSpace(): string { - return JobsStore::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + return Store::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; } } diff --git a/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php b/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php index 954233cdd3778966802b1bb235dc86299a634e82..68db88bdeed0a9453949a55c4ae639363b2e008f 100644 --- a/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php +++ b/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php @@ -6,11 +6,12 @@ use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; use PHPUnit\Framework\TestCase; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; /** * @covers \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection */ class AbstractMigrationTest extends TestCase @@ -20,14 +21,14 @@ class AbstractMigrationTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); } public function testCanInstantiateMigrationClass(): void { $this->assertInstanceOf( AbstractMigration::class, - new Version20220601000000CreateJobsTable($this->connection) + new Version20220601000000CreateJobTable($this->connection) ); } @@ -41,7 +42,7 @@ class AbstractMigrationTest extends TestCase $this->expectException(StoreException::class); - (new Version20220601000000CreateJobsTable($connectionStub)); + (new Version20220601000000CreateJobTable($connectionStub)); } public function testCanThrowGenericMigrationExceptionOnRun(): void @@ -49,7 +50,7 @@ class AbstractMigrationTest extends TestCase $migration = new class ($this->connection) extends AbstractMigration { public function run(): void { - $this->throwGenericMigrationException('test', new \Exception('test')); + throw $this->prepareGenericMigrationException('test', new \Exception('test')); } public function revert(): void @@ -61,4 +62,54 @@ class AbstractMigrationTest extends TestCase $migration->run(); } + + public function testCanUseTableNamePrefix(): void + { + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($this->connection->dbal()); + $connectionStub->method('preparePrefixedTableName')->willReturn('prefix-connection'); + + $migration = new class ($connectionStub) extends AbstractMigration { + public function run(): void + { + throw new \Exception($this->preparePrefixedTableName('table-name')); + } + public function revert(): void + { + } + protected function getLocalTablePrefix(): string + { + return 'prefix-local'; + } + }; + + try { + $migration->run(); + } catch (\Exception $exception) { + $this->assertStringContainsString('prefix-connection', $exception->getMessage()); + } + } + + public function testCanUseLocalTableNamePrefix(): void + { + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($this->connection->dbal()); + $connectionStub->method('preparePrefixedTableName')->willReturn('prefix-connection'); + + $migration = new class ($connectionStub) extends AbstractMigration { + public function run(): void + { + throw new \Exception($this->getLocalTablePrefix()); + } + public function revert(): void + { + } + }; + + try { + $migration->run(); + } catch (\Exception $exception) { + $this->assertEmpty($exception->getMessage()); + } + } } diff --git a/tests/src/Stores/Connections/DoctrineDbal/FactoryTest.php b/tests/src/Stores/Connections/DoctrineDbal/FactoryTest.php index 3790bdce74d4b819f4ecc41df718323dc4416167..2647f6962160cf7eb0a4399235e7adbba8fea699 100644 --- a/tests/src/Stores/Connections/DoctrineDbal/FactoryTest.php +++ b/tests/src/Stores/Connections/DoctrineDbal/FactoryTest.php @@ -3,11 +3,12 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Connections\DoctrineDbal; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Services\Logger; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; use PHPUnit\Framework\TestCase; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; /** * @covers \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory @@ -28,7 +29,7 @@ class FactoryTest extends TestCase { // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml). $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php'); - $this->loggerServiceMock = $this->createMock(LoggerService::class); + $this->loggerServiceMock = $this->createMock(Logger::class); } public function testCanBuildConnection(): void @@ -36,7 +37,7 @@ class FactoryTest extends TestCase /** @psalm-suppress InvalidArgument */ $factory = new Factory($this->moduleConfiguration, $this->loggerServiceMock); - $this->assertInstanceOf(Connection::class, $factory->buildConnection('doctrine_dbal_pdo_mysql')); + $this->assertInstanceOf(Connection::class, $factory->buildConnection('doctrine_dbal_pdo_sqlite')); } public function testCanBuildMigrator(): void @@ -44,7 +45,7 @@ class FactoryTest extends TestCase /** @psalm-suppress InvalidArgument */ $factory = new Factory($this->moduleConfiguration, $this->loggerServiceMock); - $connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + $connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); $this->assertInstanceOf(Migrator::class, $factory->buildMigrator($connection)); } diff --git a/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php b/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php index bfa5093c4f3af1461a29695ff47b467e54dcfdf8..008d3f139288ea393b8d86b5f1e192bc831dcf73 100644 --- a/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php +++ b/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php @@ -8,12 +8,13 @@ use Doctrine\DBAL\Schema\AbstractSchemaManager; use SimpleSAML\Module\accounting\Exceptions\InvalidValueException; use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Services\Logger; use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; use function PHPUnit\Framework\assertFalse; @@ -22,10 +23,11 @@ use function PHPUnit\Framework\assertFalse; * @covers \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000100CreateJobFailedTable * @uses \SimpleSAML\Module\accounting\ModuleConfiguration * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases\AbstractCreateJobsTable */ class MigratorTest extends TestCase { @@ -42,21 +44,12 @@ class MigratorTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); - // TODO mivanci ostavi samo sqlite verziju -// $this->connection = new Connection([ -// 'dbname' => 'accounting', -// 'user' => 'apps', -// 'password' => 'apps', -// 'host' => '127.0.0.1', -// 'port' => '33306', -// 'driver' => 'pdo_mysql', -// ]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); $this->schemaManager = $this->connection->dbal()->createSchemaManager(); $this->tableName = $this->connection->preparePrefixedTableName(Migrator::TABLE_NAME); - $this->loggerServiceMock = $this->createMock(LoggerService::class); + $this->loggerServiceMock = $this->createMock(Logger::class); // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml). $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php'); @@ -66,7 +59,7 @@ class MigratorTest extends TestCase { $this->assertFalse($this->schemaManager->tablesExist([$this->tableName])); - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $this->assertTrue($migrator->needsSetup()); @@ -84,7 +77,7 @@ class MigratorTest extends TestCase ->method('warning') ->with($this->stringContains('setup is not needed')); - /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of Logger instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $this->assertTrue($migrator->needsSetup()); @@ -100,10 +93,10 @@ class MigratorTest extends TestCase $migrator->runSetup(); - $tableNameJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); + $tableNameJobs = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); $this > assertFalse($this->schemaManager->tablesExist($tableNameJobs)); - $migrator->runMigrationClasses([JobsStore\Migrations\Version20220601000000CreateJobsTable::class]); + $migrator->runMigrationClasses([Store\Migrations\Version20220601000000CreateJobTable::class]); $this->assertTrue($this->schemaManager->tablesExist($tableNameJobs)); } @@ -212,7 +205,7 @@ class MigratorTest extends TestCase $this->expectException(StoreException::class); - $migrator->runMigrationClasses([JobsStore\Migrations\Version20220601000000CreateJobsTable::class]); + $migrator->runMigrationClasses([Store\Migrations\Version20220601000000CreateJobTable::class]); } public function testThrowsStoreExceptionOnGetImplementedMigrationClasses(): void @@ -234,11 +227,11 @@ class MigratorTest extends TestCase { return $this->moduleConfiguration->getModuleSourceDirectory() . DIRECTORY_SEPARATOR . 'Stores' . DIRECTORY_SEPARATOR . 'Jobs' . DIRECTORY_SEPARATOR . 'DoctrineDbal' . DIRECTORY_SEPARATOR . - 'JobsStore' . DIRECTORY_SEPARATOR . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + 'Store' . DIRECTORY_SEPARATOR . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; } protected function getSampleNameSpace(): string { - return JobsStore::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + return Store::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; } } diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/HashDecoratedStateTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/HashDecoratedStateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0094b57489467a56c49a3195bd1b3b5833364fd4 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/HashDecoratedStateTest.php @@ -0,0 +1,68 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\HashDecoratedState; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\HashDecoratedState + * @uses \SimpleSAML\Module\accounting\Helpers\HashHelper + * @uses \SimpleSAML\Module\accounting\Helpers\ArrayHelper + */ +class HashDecoratedStateTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub|State|State&\PHPUnit\Framework\MockObject\Stub + */ + protected $stateStub; + protected string $identityProviderEntityId; + /** + * @var string[] + */ + protected array $identityProviderMetadata; + protected string $serviceProviderEntityId; + /** + * @var string[] + */ + protected array $serviceProviderMetadata; + /** + * @var string[] + */ + protected array $attributes; + + protected function setUp(): void + { + $this->stateStub = $this->createStub(State::class); + $this->identityProviderEntityId = 'idpEntityId'; + $this->stateStub->method('getIdentityProviderEntityId')->willReturn($this->identityProviderEntityId); + $this->identityProviderMetadata = ['idp' => 'metadata']; + $this->stateStub->method('getIdentityProviderMetadata')->willReturn($this->identityProviderMetadata); + $this->serviceProviderEntityId = 'spEntityId'; + $this->stateStub->method('getServiceProviderEntityId')->willReturn($this->serviceProviderEntityId); + $this->serviceProviderMetadata = ['sp' => 'metadata']; + $this->stateStub->method('getServiceProviderMetadata')->willReturn($this->serviceProviderMetadata); + $this->attributes = ['sample' => 'attribute']; + $this->stateStub->method('getAttributes')->willReturn($this->attributes); + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf(HashDecoratedState::class, new HashDecoratedState($this->stateStub)); + } + + public function testCanGetHashedProperties(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $hashDecoratedState = new HashDecoratedState($this->stateStub); + + $this->assertSame($this->stateStub, $hashDecoratedState->getState()); + $this->assertIsString($hashDecoratedState->getIdentityProviderEntityIdHashSha256()); + $this->assertIsString($hashDecoratedState->getServiceProviderEntityIdHashSha256()); + $this->assertIsString($hashDecoratedState->getIdentityProviderMetadataArrayHashSha256()); + $this->assertIsString($hashDecoratedState->getServiceProviderMetadataArrayHashSha256()); + $this->assertIsString($hashDecoratedState->getAttributesArrayHashSha256()); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000000CreateIdpTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000000CreateIdpTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2b99a6106c635b609e386f119f3f3ee5b733d47f --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000000CreateIdpTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000000CreateIdpTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + */ +class Version20220801000000CreateIdpTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_idp'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000000CreateIdpTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000000CreateIdpTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000000CreateIdpTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000000CreateIdpTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000100CreateIdpVersionTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000100CreateIdpVersionTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..525d7ef8226b7848679136175706122fcc92842f --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000100CreateIdpVersionTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000100CreateIdpVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000100CreateIdpVersionTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_idp_version'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000100CreateIdpVersionTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000100CreateIdpVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000100CreateIdpVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000100CreateIdpVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000200CreateSpTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000200CreateSpTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..11d7874bb33138093b49fb518f595dd9e8350c06 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000200CreateSpTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000200CreateSpTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000200CreateSpTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_sp'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000200CreateSpTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000200CreateSpTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000200CreateSpTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000200CreateSpTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000300CreateSpVersionTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000300CreateSpVersionTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a839507c55379830055302d14a59ad639c16b344 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000300CreateSpVersionTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000300CreateSpVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000300CreateSpVersionTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_sp_version'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000300CreateSpVersionTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000300CreateSpVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000300CreateSpVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000300CreateSpVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000400CreateUserTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000400CreateUserTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..eca699c62deb3ea3a84fa044b90ba0b58b59cbe0 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000400CreateUserTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000400CreateUserTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000400CreateUserTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_user'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000400CreateUserTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000400CreateUserTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000400CreateUserTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000400CreateUserTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000500CreateUserVersionTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000500CreateUserVersionTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..42384894bdc8596fbcc51848d600c09ec02c112a --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000500CreateUserVersionTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000500CreateUserVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000500CreateUserVersionTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_user_version'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000500CreateUserVersionTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000500CreateUserVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000500CreateUserVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000500CreateUserVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000600CreateIdpSpUserVersionTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000600CreateIdpSpUserVersionTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ce214e658f70dbdeb91a971cc25c95f7769a7ed5 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000600CreateIdpSpUserVersionTableTest.php @@ -0,0 +1,88 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000600CreateIdpSpUserVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000600CreateIdpSpUserVersionTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_idp_sp_user_version'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = + new Migrations\Version20220801000600CreateIdpSpUserVersionTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = + new Migrations\Version20220801000600CreateIdpSpUserVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = + new Migrations\Version20220801000600CreateIdpSpUserVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = + new Migrations\Version20220801000600CreateIdpSpUserVersionTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTableTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..004b1923c03af384e30e1e941861fdddd665f20f --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTableTest.php @@ -0,0 +1,84 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; + +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000700CreateAuthenticationEventTable + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * + */ +class Version20220801000700CreateAuthenticationEventTableTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; + protected string $tableName; + protected \PHPUnit\Framework\MockObject\Stub $connectionStub; + protected \PHPUnit\Framework\MockObject\Stub $dbalStub; + protected \PHPUnit\Framework\MockObject\Stub $schemaManagerStub; + + protected function setUp(): void + { + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + $this->tableName = 'vds_authentication_event'; + + $this->connectionStub = $this->createStub(Connection::class); + $this->dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $this->schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + } + + public function testCanRunMigration(): void + { + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + $migration = new Migrations\Version20220801000700CreateAuthenticationEventTable($this->connection); + $migration->run(); + $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); + $migration->revert(); + $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); + } + + public function testRunThrowsMigrationException(): void + { + $this->connectionStub->method('preparePrefixedTableName')->willReturn($this->tableName); + $this->schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000700CreateAuthenticationEventTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $this->schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $this->dbalStub->method('createSchemaManager')->willReturn($this->schemaManagerStub); + $this->connectionStub->method('dbal')->willReturn($this->dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000700CreateAuthenticationEventTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } + + public function testRunThrowsOnIvalidTableNameIdp(): void + { + $this->connectionStub->method('preparePrefixedTableName') + ->willReturnOnConsecutiveCalls(''); // Invalid (empty) name for table + + /** @psalm-suppress InvalidArgument */ + $migration = new Migrations\Version20220801000700CreateAuthenticationEventTable($this->connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2999244c0ddd8e302190767482b1aba675f7c972 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php @@ -0,0 +1,130 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawActivity; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; +use SimpleSAML\Test\Module\accounting\Constants\DateTime; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawActivity + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + */ +class RawActivityTest extends TestCase +{ + /** + * @var string[] + */ + protected array $serviceProviderMetadata; + /** + * @var string[] + */ + protected array $userAttributes; + protected string $happenedAt; + protected array $rawRow; + /** + * @var AbstractPlatform|AbstractPlatform&\PHPUnit\Framework\MockObject\Stub|\PHPUnit\Framework\MockObject\Stub + */ + protected $abstractPlatformStub; + + protected function setUp(): void + { + $this->serviceProviderMetadata = ['sp' => 'metadata']; + $this->userAttributes = ['user' => 'attribute']; + $this->happenedAt = '2022-02-22 22:22:22'; + $this->rawRow = [ + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA => serialize($this->serviceProviderMetadata), + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES => serialize($this->userAttributes), + TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT => $this->happenedAt, + ]; + $this->abstractPlatformStub = $this->createStub(AbstractPlatform::class); + $this->abstractPlatformStub->method('getDateTimeFormatString')->willReturn(DateTime::DEFAULT_FORMAT); + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $rawActivity = new RawActivity($this->rawRow, $this->abstractPlatformStub); + + $this->assertInstanceOf(RawActivity::class, $rawActivity); + } + + public function testCanGetProperties(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $rawActivity = new RawActivity($this->rawRow, $this->abstractPlatformStub); + + $this->assertInstanceOf(\DateTimeImmutable::class, $rawActivity->getHappenedAt()); + $this->assertSame($this->serviceProviderMetadata, $rawActivity->getServiceProviderMetadata()); + $this->assertSame($this->userAttributes, $rawActivity->getUserAttributes()); + } + + public function testThrowsIfColumnNotPresent(): void + { + $rawRow = $this->rawRow; + unset($rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT]); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawActivity($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsForNonStringServiceProviderMetadata(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawActivity($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsForNonStringUserAttributes(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawActivity($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsForNonStringHappenedAt(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawActivity($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsForInvalidServiceProviderMetadata(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_SP_METADATA] = serialize(1); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawActivity($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsForInvalidUserAttributes(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_USER_ATTRIBUTES] = serialize(1); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawActivity($rawRow, $this->abstractPlatformStub); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawConnectedServiceProviderTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawConnectedServiceProviderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..68fe4ed768439f1b39228591cdb6e8ef1c898d84 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawConnectedServiceProviderTest.php @@ -0,0 +1,177 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawConnectedServiceProvider; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; +use SimpleSAML\Test\Module\accounting\Constants\DateTime; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawConnectedServiceProvider + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + */ +class RawConnectedServiceProviderTest extends TestCase +{ + protected int $numberOfAuthentications; + protected string $lastAuthenticationAt; + protected string $firstAuthenticationAt; + /** + * @var string[] + */ + protected array $serviceProviderMetadata; + /** + * @var string[] + */ + protected array $userAttributes; + protected array $rawRow; + /** + * @var AbstractPlatform|AbstractPlatform&\PHPUnit\Framework\MockObject\Stub|\PHPUnit\Framework\MockObject\Stub + */ + protected $abstractPlatformStub; + protected string $dateTimeFormat; + + protected function setUp(): void + { + $this->numberOfAuthentications = 2; + $this->lastAuthenticationAt = '2022-02-22 22:22:22'; + $this->firstAuthenticationAt = '2022-02-02 22:22:22'; + $this->serviceProviderMetadata = ['sp' => 'metadata']; + $this->userAttributes = ['user' => 'attribute']; + $this->rawRow = [ + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS => + $this->numberOfAuthentications, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT => + $this->lastAuthenticationAt, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT => + $this->firstAuthenticationAt, + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA => + serialize($this->serviceProviderMetadata), + TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES => + serialize($this->userAttributes), + ]; + $this->dateTimeFormat = DateTime::DEFAULT_FORMAT; + $this->abstractPlatformStub = $this->createStub(AbstractPlatform::class); + $this->abstractPlatformStub->method('getDateTimeFormatString') + ->willReturn($this->dateTimeFormat); + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + RawConnectedServiceProvider::class, + new RawConnectedServiceProvider($this->rawRow, $this->abstractPlatformStub) + ); + } + + public function testCanGetProperties(): void + { + /** @psalm-suppress PossiblyInvalidArgument */ + $rawConnectedServiceProvider = new RawConnectedServiceProvider($this->rawRow, $this->abstractPlatformStub); + + $this->assertSame($this->numberOfAuthentications, $rawConnectedServiceProvider->getNumberOfAuthentications()); + $this->assertInstanceOf(\DateTimeImmutable::class, $rawConnectedServiceProvider->getLastAuthenticationAt()); + $this->assertSame( + $this->lastAuthenticationAt, + $rawConnectedServiceProvider->getLastAuthenticationAt()->format($this->dateTimeFormat) + ); + $this->assertInstanceOf(\DateTimeImmutable::class, $rawConnectedServiceProvider->getFirstAuthenticationAt()); + $this->assertSame( + $this->firstAuthenticationAt, + $rawConnectedServiceProvider->getFirstAuthenticationAt()->format($this->dateTimeFormat) + ); + $this->assertSame($this->serviceProviderMetadata, $rawConnectedServiceProvider->getServiceProviderMetadata()); + $this->assertSame($this->userAttributes, $rawConnectedServiceProvider->getUserAttributes()); + } + + public function testThrowsIfColumnNotSet(): void + { + $rawRow = $this->rawRow; + unset($rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES]); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfNumberOfAuthenticationsNotNumeric(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS] = 'a'; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfLastAuthenticationAtNotString(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_LAST_AUTHENTICATION_AT] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfFirstAuthenticationAtNotString(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_FIRST_AUTHENTICATION_AT] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfSpMetadataNotString(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfUserAttributesNotString(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] = 1; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfSpMetadataNotValid(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] = serialize(1); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } + + public function testThrowsIfUserAttributesNotValid(): void + { + $rawRow = $this->rawRow; + $rawRow[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] = serialize(1); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + new RawConnectedServiceProvider($rawRow, $this->abstractPlatformStub); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2cd94b37a3698ae46805ac95ce47662e7e84ad4b --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php @@ -0,0 +1,687 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; +use SimpleSAML\Test\Module\accounting\Constants\DateTime; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository + * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper + * @uses \SimpleSAML\Module\accounting\ModuleConfiguration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000000CreateIdpTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000100CreateIdpVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000200CreateSpTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000300CreateSpVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000400CreateUserTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000500CreateUserVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000600CreateIdpSpUserVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000700CreateAuthenticationEventTable + * + * @psalm-suppress all + */ +class RepositoryTest extends TestCase +{ + protected Connection $connection; + protected \Doctrine\DBAL\Connection $dbal; + /** + * @var \PHPUnit\Framework\MockObject\Stub|LoggerInterface|LoggerInterface&\PHPUnit\Framework\MockObject\Stub + */ + protected $loggerStub; + protected Migrator $migrator; + protected string $dateTimeFormat; + protected string $idpEntityId; + protected string $idpEntityIdHash; + protected string $idpMetadata; + protected string $idpMetadataHash; + protected string $spEntityId; + protected string $spMetadataHash; + protected string $userIdentifier; + protected string $userIdentifierHash; + protected string $userAttributes; + protected string $userAttributesHash; + protected Repository $repository; + protected \DateTimeImmutable $createdAt; + /** + * @var \PHPUnit\Framework\MockObject\Stub|Connection|Connection&\PHPUnit\Framework\MockObject\Stub + */ + protected $connectionStub; + protected string $spEntityIdHash; + protected string $spMetadata; + + protected function setUp(): void + { + // For stubbing. + $this->connectionStub = $this->createStub(Connection::class); + $this->loggerStub = $this->createStub(LoggerInterface::class); + + // For real DB testing. + $connectionParameters = ConnectionParameters::DBAL_SQLITE_MEMORY; + $this->connection = new Connection($connectionParameters); + $this->migrator = new Migrator($this->connection, $this->loggerStub); + $moduleConfiguration = new ModuleConfiguration(); + $migrationsDirectory = $moduleConfiguration->getModuleSourceDirectory() . DIRECTORY_SEPARATOR . 'Stores' . + DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR . 'Authentication' . DIRECTORY_SEPARATOR . + 'DoctrineDbal' . DIRECTORY_SEPARATOR . 'Versioned' . DIRECTORY_SEPARATOR . 'Store' . DIRECTORY_SEPARATOR . + AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + $namespace = Store::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + + $this->migrator->runSetup(); + $this->migrator->runNonImplementedMigrationClasses($migrationsDirectory, $namespace); + + $this->repository = new Repository($this->connection, $this->loggerStub); + + $this->dateTimeFormat = DateTime::DEFAULT_FORMAT; + + $this->idpEntityId = 'idp-entity-id'; + $this->idpEntityIdHash = 'idp-entity-id-hash'; + + $this->idpMetadata = 'idp-metadata'; + $this->idpMetadataHash = 'idp-metadata-hash'; + + $this->spEntityId = 'sp-entity-id'; + $this->spEntityIdHash = 'sp-entity-id-hash'; + + $this->spMetadata = 'sp-metadata'; + $this->spMetadataHash = 'sp-metadata-hash'; + + $this->userIdentifier = 'user-identifier'; + $this->userIdentifierHash = 'user-identifier-hash'; + + $this->userAttributes = 'user-attributes'; + $this->userAttributesHash = 'user-attributes-hash'; + + $this->createdAt = new \DateTimeImmutable(); + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf( + Repository::class, + new Repository($this->connection, $this->loggerStub) + ); + } + + public function testCanInsertAndGetIdp(): array + { + $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt); + + $result = $this->repository->getIdp($this->idpEntityIdHash)->fetchAssociative(); + + $this->assertSame($this->idpEntityId, $result[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID]); + $this->assertSame( + $this->idpEntityIdHash, + $result[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ENTITY_ID_HASH_SHA256] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_IDP_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertIdpThrowsOnNonUniqueIdpEntityIdHash(): void + { + $this->expectException(StoreException::class); + + // Can't insert duplicate idp entity ID hash. + $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt); + $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt); + } + + public function testGetIdpThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getIdp($this->idpEntityIdHash); + } + + /** + * @depends testCanInsertAndGetIdp + */ + public function testCanInsertAndGetIdpVersion(array $idpResult): array + { + $idpId = (int)$idpResult[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ID]; + + $this->repository->insertIdpVersion($idpId, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt); + + $result = $this->repository->getIdpVersion($idpId, $this->idpMetadataHash)->fetchAssociative(); + + $this->assertSame($this->idpMetadata, $result[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA]); + $this->assertSame( + $this->idpMetadataHash, + $result[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertIdpVersionThrowsOnNonUniqueIdpMetadataHash(): void + { + $this->expectException(StoreException::class); + // IdP Metadata Hash must be unique. + $this->repository->insertIdpVersion(1, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt); + $this->repository->insertIdpVersion(1, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt); + } + + public function testGetIdpVersionThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getIdpVersion(1, $this->idpMetadataHash); + } + + public function testCanInsertAndGetSp(): array + { + $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt); + + $result = $this->repository->getSp($this->spEntityIdHash)->fetchAssociative(); + + $this->assertSame($this->spEntityId, $result[Store\TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID]); + $this->assertSame( + $this->spEntityIdHash, + $result[Store\TableConstants::TABLE_SP_COLUMN_NAME_ENTITY_ID_HASH_SHA256] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_SP_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertSpThrowsOnNonUniqueSpEntityIdHash(): void + { + $this->expectException(StoreException::class); + // SP Entity ID Hash must be unique. + $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt); + $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt); + } + + public function testGetSpThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getSp($this->spEntityIdHash); + } + + /** + * @depends testCanInsertAndGetSp + */ + public function testCanInsertAndGetSpVersion(array $spResult): array + { + $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID]; + + $this->repository->insertSpVersion($spId, $this->spMetadata, $this->spMetadataHash, $this->createdAt); + + $result = $this->repository->getSpVersion($spId, $this->spMetadataHash)->fetchAssociative(); + + $this->assertSame($this->spMetadata, $result[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA]); + $this->assertSame( + $this->spMetadataHash, + $result[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_METADATA_HASH_SHA256] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertSpVersionThrowsOnNonUniqueMetadataHash(): void + { + $this->expectException(StoreException::class); + // SP metadata hash must be unique. + $this->repository->insertSpVersion(1, $this->spMetadata, $this->spMetadataHash, $this->createdAt); + $this->repository->insertSpVersion(1, $this->spMetadata, $this->spMetadataHash, $this->createdAt); + } + + public function testGetSpVersionThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getSpVersion(1, $this->spMetadataHash); + } + + public function testCanInsertAndGetUser(): array + { + $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt); + + $result = $this->repository->getUser($this->userIdentifierHash)->fetchAssociative(); + + $this->assertSame($this->userIdentifier, $result[Store\TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER]); + $this->assertSame( + $this->userIdentifierHash, + $result[Store\TableConstants::TABLE_USER_COLUMN_NAME_IDENTIFIER_HASH_SHA256] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_USER_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertUserThrowsOnNonUniqueIdentifierHash(): void + { + $this->expectException(StoreException::class); + $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt); + $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt); + } + + public function testGetUserThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getUser($this->userIdentifierHash); + } + + /** + * @depends testCanInsertAndGetUser + */ + public function testCanInsertAndGetUserVersion(array $userResult): array + { + $userId = (int)$userResult[Store\TableConstants::TABLE_USER_COLUMN_NAME_ID]; + + $this->repository + ->insertUserVersion($userId, $this->userAttributes, $this->userAttributesHash, $this->createdAt); + + $result = $this->repository->getUserVersion($userId, $this->userAttributesHash)->fetchAssociative(); + + $this->assertSame( + $this->userAttributes, + $result[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES] + ); + $this->assertSame( + $this->userAttributesHash, + $result[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ATTRIBUTES_HASH_SHA256] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertUserVersionThrowsOnNonUniqueAttributesHash(): void + { + $this->expectException(StoreException::class); + $this->repository + ->insertUserVersion(1, $this->userAttributes, $this->userAttributesHash, $this->createdAt); + $this->repository + ->insertUserVersion(1, $this->userAttributes, $this->userAttributesHash, $this->createdAt); + } + + public function testGetUserVersionThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getUserVersion(1, $this->userIdentifierHash); + } + + /** + * @depends testCanInsertAndGetIdpVersion + * @depends testCanInsertAndGetSpVersion + * @depends testCanInsertAndGetUserVersion + */ + public function testCanInsertAndGetIdpSpUserVersion( + array $idpVersionResult, + array $spVersionResult, + array $userVersionResult + ): array { + $idpVersionId = (int)$idpVersionResult[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_ID]; + $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID]; + $userVersionId = (int)$userVersionResult[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + $result = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + + $this->assertSame( + $idpVersionId, + (int)$result[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_IDP_VERSION_ID] + ); + $this->assertSame( + $spVersionId, + (int)$result[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID] + ); + $this->assertSame( + $userVersionId, + (int)$result[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_SP_VERSION_ID] + ); + $this->assertSame( + $this->createdAt->format($this->dateTimeFormat), + $result[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_CREATED_AT] + ); + + return $result; + } + + public function testInsertIdpSpUserVersionThrowsOnNonUnique(): void + { + $this->expectException(StoreException::class); + $this->repository->insertIdpSpUserVersion(1, 1, 1, $this->createdAt); + $this->repository->insertIdpSpUserVersion(1, 1, 1, $this->createdAt); + } + + public function testGetIdpSpUserVersionThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getIdpSpUserVersion(1, 1, 1); + } + + /** + * @depends testCanInsertAndGetIdpSpUserVersion + */ + public function testCanInsertAuthenticationEvent(array $idpSpUserVersionResult): void + { + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + $createdAt = $happenedAt = new \DateTimeImmutable(); + + $authenticationEventCounterQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $authenticationEventCounterQueryBuilder->select('COUNT(id) as authenticationEventCount') + ->from( + Store\TableConstants::TABLE_PREFIX . + $this->connection + ->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_AUTHENTICATION_EVENT) + ); + + $this->assertSame(0, (int)$authenticationEventCounterQueryBuilder->executeQuery()->fetchOne()); + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $happenedAt, $createdAt); + + $this->assertSame(1, (int)$authenticationEventCounterQueryBuilder->executeQuery()->fetchOne()); + } + + public function testInsertAuthenticationEventThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->insertAuthenticationEvent(1, $this->createdAt); + } + + public function testCanGetConnectedServiceProviders(): void + { + $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt); + $idpResult = $this->repository->getIdp($this->idpEntityIdHash)->fetchAssociative(); + $idpId = (int)$idpResult[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ID]; + $this->repository->insertIdpVersion($idpId, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt); + $idpVersionResult = $this->repository->getIdpVersion($idpId, $this->idpMetadataHash)->fetchAssociative(); + + $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt); + $spResult = $this->repository->getSp($this->spEntityIdHash)->fetchAssociative(); + $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID]; + $this->repository->insertSpVersion($spId, $this->spMetadata, $this->spMetadataHash, $this->createdAt); + $spVersionResult = $this->repository->getSpVersion($spId, $this->spMetadataHash)->fetchAssociative(); + + $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt); + $userResult = $this->repository->getUser($this->userIdentifierHash)->fetchAssociative(); + $userId = (int)$userResult[Store\TableConstants::TABLE_USER_COLUMN_NAME_ID]; + $this->repository + ->insertUserVersion($userId, $this->userAttributes, $this->userAttributesHash, $this->createdAt); + $userVersionResult = $this->repository->getUserVersion($userId, $this->userAttributesHash)->fetchAssociative(); + + $idpVersionId = (int)$idpVersionResult[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_ID]; + $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID]; + $userVersionId = (int)$userVersionResult[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + + $resultArray = $this->repository->getConnectedServiceProviders($this->userIdentifierHash); + + $this->assertCount(1, $resultArray); + $this->assertSame( + '1', + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS] + ); + $this->assertSame( + $this->spMetadata, + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] + ); + $this->assertSame( + $this->userAttributes, + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] + ); + + $resultArray = $this->repository->getConnectedServiceProviders($this->userIdentifierHash); + $this->assertCount(1, $resultArray); + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + $resultArray = $this->repository->getConnectedServiceProviders($this->userIdentifierHash); + $this->assertCount(1, $resultArray); + $this->assertSame( + '2', + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS] + ); + $this->assertSame( + $this->spMetadata, + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] + ); + $this->assertSame( + $this->userAttributes, + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] + ); + + // Simulate another SP + $spEntityIdNew = $this->spEntityId . '-new'; + $spEntityIdHashNew = $this->spEntityIdHash . '-new'; + $spMetadataNew = $this->spMetadata . '-new'; + $spMetadataHashNew = $this->spMetadataHash . '-new'; + $this->repository->insertSp($spEntityIdNew, $spEntityIdHashNew, $this->createdAt); + $spResult = $this->repository->getSp($spEntityIdHashNew)->fetchAssociative(); + $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID]; + $this->repository->insertSpVersion($spId, $spMetadataNew, $spMetadataHashNew, $this->createdAt); + $spVersionResult = $this->repository->getSpVersion($spId, $spMetadataHashNew)->fetchAssociative(); + $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + + $resultArray = $this->repository->getConnectedServiceProviders($this->userIdentifierHash); + $this->assertCount(2, $resultArray); + $this->assertSame( + '1', + $resultArray[$spEntityIdNew] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS] + ); + $this->assertSame( + $spMetadataNew, + $resultArray[$spEntityIdNew] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] + ); + $this->assertSame( + $this->userAttributes, + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] + ); + + // Simulate change in user attributes + $userAttributesNew = $this->userAttributes . '-new'; + $userAttributesHashNew = $this->userAttributesHash . '-new'; + $this->repository->insertUserVersion($userId, $userAttributesNew, $userAttributesHashNew, $this->createdAt); + $userVersionResult = $this->repository->getUserVersion($userId, $userAttributesHashNew)->fetchAssociative(); + $userVersionId = (int)$userVersionResult[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + + $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + $resultArray = $this->repository->getConnectedServiceProviders($this->userIdentifierHash); + + $this->assertCount(2, $resultArray); + $this->assertSame( + '2', + $resultArray[$spEntityIdNew] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS] + ); + $this->assertSame( + $spMetadataNew, + $resultArray[$spEntityIdNew] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_SP_METADATA] + ); + // New SP with new user attributes version.. + $this->assertSame( + $userAttributesNew, + $resultArray[$spEntityIdNew] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] + ); + + // First SP still has old user attributes version... + $this->assertSame( + $this->userAttributes, + $resultArray[$this->spEntityId] + [Store\TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_USER_ATTRIBUTES] + ); + } + + public function testGetConnectedServiceProvidersThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getConnectedServiceProviders($this->userIdentifierHash); + } + + public function testCanGetActivity(): void + { + $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt); + $idpResult = $this->repository->getIdp($this->idpEntityIdHash)->fetchAssociative(); + $idpId = (int)$idpResult[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ID]; + $this->repository->insertIdpVersion($idpId, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt); + $idpVersionResult = $this->repository->getIdpVersion($idpId, $this->idpMetadataHash)->fetchAssociative(); + + $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt); + $spResult = $this->repository->getSp($this->spEntityIdHash)->fetchAssociative(); + $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID]; + $this->repository->insertSpVersion($spId, $this->spMetadata, $this->spMetadataHash, $this->createdAt); + $spVersionResult = $this->repository->getSpVersion($spId, $this->spMetadataHash)->fetchAssociative(); + + $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt); + $userResult = $this->repository->getUser($this->userIdentifierHash)->fetchAssociative(); + $userId = (int)$userResult[Store\TableConstants::TABLE_USER_COLUMN_NAME_ID]; + $this->repository + ->insertUserVersion($userId, $this->userAttributes, $this->userAttributesHash, $this->createdAt); + $userVersionResult = $this->repository->getUserVersion($userId, $this->userAttributesHash)->fetchAssociative(); + + $idpVersionId = (int)$idpVersionResult[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_ID]; + $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID]; + $userVersionId = (int)$userVersionResult[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + + $resultArray = $this->repository->getActivity($this->userIdentifierHash); + $this->assertCount(1, $resultArray); + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + $resultArray = $this->repository->getActivity($this->userIdentifierHash); + $this->assertCount(2, $resultArray); + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + $resultArray = $this->repository->getActivity($this->userIdentifierHash); + $this->assertCount(3, $resultArray); + + // Simulate another SP + $spEntityIdNew = $this->spEntityId . '-new'; + $spEntityIdHashNew = $this->spEntityIdHash . '-new'; + $spMetadataNew = $this->spMetadata . '-new'; + $spMetadataHashNew = $this->spMetadataHash . '-new'; + $this->repository->insertSp($spEntityIdNew, $spEntityIdHashNew, $this->createdAt); + $spResult = $this->repository->getSp($spEntityIdHashNew)->fetchAssociative(); + $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID]; + $this->repository->insertSpVersion($spId, $spMetadataNew, $spMetadataHashNew, $this->createdAt); + $spVersionResult = $this->repository->getSpVersion($spId, $spMetadataHashNew)->fetchAssociative(); + $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertAuthenticationEvent($idpSpUserVersionId, $this->createdAt, $this->createdAt); + $resultArray = $this->repository->getActivity($this->userIdentifierHash); + $this->assertCount(4, $resultArray); + + // Simulate a change in user attributes + } + + public function testGetActivityThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->getActivity($this->userIdentifierHash); + } +} diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b7506f38917105ee8febbb2df59944ce6f45f2e9 --- /dev/null +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php @@ -0,0 +1,627 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned; + +use Doctrine\DBAL\Result; +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\TableConstants; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; +use SimpleSAML\Test\Module\accounting\Constants\RawRowResult; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000000CreateIdpTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000100CreateIdpVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000200CreateSpTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000300CreateSpVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000400CreateUserTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000500CreateUserVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000600CreateIdpSpUserVersionTable + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Migrations\Version20220801000700CreateAuthenticationEventTable + * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\HashDecoratedState + * @uses \SimpleSAML\Module\accounting\Helpers\HashHelper + * @uses \SimpleSAML\Module\accounting\Helpers\ArrayHelper + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper + * @uses \SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider\Bag + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + * @uses \SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider + * @uses \SimpleSAML\Module\accounting\Entities\User + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawConnectedServiceProvider + * @uses \SimpleSAML\Module\accounting\Entities\Activity\Bag + * @uses \SimpleSAML\Module\accounting\Entities\Activity + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawActivity + * + * @psalm-suppress all + */ +class StoreTest extends TestCase +{ + protected \PHPUnit\Framework\MockObject\Stub $moduleConfigurationStub; + protected Migrator $migrator; + protected \PHPUnit\Framework\MockObject\Stub $factoryStub; + protected Connection $connection; + protected State $state; + protected Event $authenticationEvent; + protected Store\HashDecoratedState $hashDecoratedState; + /** + * @var \PHPUnit\Framework\MockObject\Stub|Store\Repository|Store\Repository&\PHPUnit\Framework\MockObject\Stub + */ + protected $repositoryStub; + /** + * @var Result|Result&\PHPUnit\Framework\MockObject\Stub|\PHPUnit\Framework\MockObject\Stub + */ + protected $resultStub; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface|LoggerInterface&\PHPUnit\Framework\MockObject\MockObject + */ + protected $loggerMock; + + protected function setUp(): void + { + $connectionParams = ConnectionParameters::DBAL_SQLITE_MEMORY; + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn($connectionParams); + $this->moduleConfigurationStub->method('getUserIdAttributeName') + ->willReturn('hrEduPersonPersistentID'); + + $this->connection = new Connection($connectionParams); + + $this->loggerMock = $this->createMock(LoggerInterface::class); + + /** @psalm-suppress InvalidArgument */ + $this->migrator = new Migrator($this->connection, $this->loggerMock); + + $this->factoryStub = $this->createStub(Factory::class); + $this->factoryStub->method('buildConnection')->willReturn($this->connection); + $this->factoryStub->method('buildMigrator')->willReturn($this->migrator); + + $this->state = new State(StateArrays::FULL); + $this->authenticationEvent = new Event($this->state); + + $this->hashDecoratedState = new Store\HashDecoratedState($this->state); + $this->repositoryStub = $this->createStub(Store\Repository::class); + + $this->resultStub = $this->createStub(Result::class); + } + + public function testCanConstructInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf( + Store::class, + new Store($this->moduleConfigurationStub, $this->loggerMock, $this->factoryStub) + ); + } + + public function testCanBuildInstance(): void + { + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf( + Store::class, + Store::build($this->moduleConfigurationStub, $this->loggerMock) + ); + } + + public function testCanPersistAuthenticationEvent(): void + { + /** @psalm-suppress InvalidArgument */ + $store = new Store($this->moduleConfigurationStub, $this->loggerMock, $this->factoryStub); + $store->runSetup(); + + $idpCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $idpVersionCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $spCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $spVersionCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $userCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $userVersionCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $idpSpUserVersionCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + $authenticationEventCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $idpCountQueryBuilder->select('COUNT(id) as idpCount')->from( + //'vds_idp' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_IDP + ) + ); + $idpVersionCountQueryBuilder->select('COUNT(id) as idpVersionCount')->from( + //'vds_idp_version' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_IDP_VERSION + ) + ); + $spCountQueryBuilder->select('COUNT(id) as spCount')->from( + //'vds_sp' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_SP + ) + ); + $spVersionCountQueryBuilder->select('COUNT(id) as spVersionCount')->from( + //'vds_sp_version' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_SP_VERSION + ) + ); + $userCountQueryBuilder->select('COUNT(id) as userCount')->from( + //'vds_user' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_USER + ) + ); + $userVersionCountQueryBuilder->select('COUNT(id) as userVersionCount')->from( + //'vds_user_version' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_USER_VERSION + ) + ); + $idpSpUserVersionCountQueryBuilder->select('COUNT(id) as idpSpUserVersionCount') + ->from( + //'vds_idp_sp_user_version' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_IDP_SP_USER_VERSION + ) + ); + $authenticationEventCountQueryBuilder->select('COUNT(id) as authenticationEventCount') + ->from( + //'vds_authentication_event' + $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_PREFIX . Store\TableConstants::TABLE_NAME_AUTHENTICATION_EVENT + ) + ); + + $this->assertSame(0, (int)$idpCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$idpVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$spCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$spVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$userCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$userVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$idpSpUserVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(0, (int)$authenticationEventCountQueryBuilder->executeQuery()->fetchOne()); + + $store->persist($this->authenticationEvent); + + $this->assertSame(1, (int)$idpCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$idpVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$spCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$spVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$userCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$userVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$idpSpUserVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$authenticationEventCountQueryBuilder->executeQuery()->fetchOne()); + + $store->persist($this->authenticationEvent); + + $this->assertSame(1, (int)$idpCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$idpVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$spCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$spVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$userCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$userVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(1, (int)$idpSpUserVersionCountQueryBuilder->executeQuery()->fetchOne()); + $this->assertSame(2, (int)$authenticationEventCountQueryBuilder->executeQuery()->fetchOne()); + } + + public function testResolveIdpIdThrowsOnFirstGetIdpFailure(): void + { + $this->repositoryStub->method('getIdp')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveIdpIdThrowsOnInsertAndGetIdpFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getIdp')->willReturn($this->resultStub); + $this->repositoryStub->method('insertIdp')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testResolveIdpVersionIdThrowsOnFirstGetIdpVersionFailure(): void + { + $this->repositoryStub->method('getIdpVersion')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveIdpVersionIdThrowsOnInsertAndGetIdpVersionFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getIdpVersion')->willReturn($this->resultStub); + $this->repositoryStub->method('insertIdpVersion')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testResolveSpIdThrowsOnFirstGetSpFailure(): void + { + $this->repositoryStub->method('getSp')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveSpIdThrowsOnInsertAndGetSpFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getSp')->willReturn($this->resultStub); + $this->repositoryStub->method('insertSp')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testResolveSpVersionIdThrowsOnFirstGetSpVersionFailure(): void + { + $this->repositoryStub->method('getSpVersion')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveSpVersionIdThrowsOnInsertAndGetSpVersionFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getSpVersion')->willReturn($this->resultStub); + $this->repositoryStub->method('insertSpVersion')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testResolveUserIdThrowsOnInvalidUserIdentifierValue(): void + { + $moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $moduleConfigurationStub->method('getUserIdAttributeName')->willReturn('invalid'); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(UnexpectedValueException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveUserIdThrowsOnFirstGetUserFailure(): void + { + $this->repositoryStub->method('getUser')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveUserIdThrowsOnInsertAndGetUserFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getUser')->willReturn($this->resultStub); + $this->repositoryStub->method('insertUser')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testResolveUserVersionIdThrowsOnFirstGetUserVersionFailure(): void + { + $this->repositoryStub->method('getUserVersion')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveUserVersionIdThrowsOnInsertAndGetUserVersionFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getUserVersion')->willReturn($this->resultStub); + $this->repositoryStub->method('insertUserVersion')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testResolveIdpSpUserVersionIdThrowsOnFirstGetIdpSpUserVersionFailure(): void + { + $this->repositoryStub->method('getIdpSpUserVersion')->willThrowException(new \Exception('test')); + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + + $store->persist($this->authenticationEvent); + } + + public function testResolveIdpSpUserVersionIdThrowsOnInsertAndGetIdpSpUserVersionFailure(): void + { + $this->resultStub->method('fetchOne')->willReturn(false); + $this->repositoryStub->method('getIdpSpUserVersion')->willReturn($this->resultStub); + $this->repositoryStub->method('insertIdpSpUserVersion')->willThrowException(new \Exception('test')); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $this->loggerMock->expects($this->once())->method('warning'); + + $store->persist($this->authenticationEvent); + } + + public function testGetConnectedOrganizationsReturnsEmptyBagIfNoResults(): void + { + $this->repositoryStub->method('getConnectedServiceProviders')->willReturn([]); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $connectedServiceProviderBag = $store->getConnectedOrganizations('test'); + + $this->assertEmpty($connectedServiceProviderBag->getAll()); + } + + public function testCanGetConnectedOrganizationsBag(): void + { + $this->repositoryStub->method('getConnectedServiceProviders') + ->willReturn([RawRowResult::CONNECTED_ORGANIZATION]); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $connectedServiceProviderBag = $store->getConnectedOrganizations('test'); + + $this->assertNotEmpty($connectedServiceProviderBag->getAll()); + } + + public function testGetConnectedOrganizationsThrowsForInvalidResult(): void + { + $rawResult = RawRowResult::CONNECTED_ORGANIZATION; + unset($rawResult[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS]); + + $this->repositoryStub->method('getConnectedServiceProviders') + ->willReturn([$rawResult]); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $store->getConnectedOrganizations('test'); + } + + public function testGetActivityReturnsEmptyBagIfNoResults(): void + { + $this->repositoryStub->method('getActivity')->willReturn([]); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $activityBag = $store->getActivity('test'); + + $this->assertEmpty($activityBag->getAll()); + } + + public function testCanGetActivityBag(): void + { + $this->repositoryStub->method('getActivity') + ->willReturn([RawRowResult::ACTIVITY]); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $activityBag = $store->getActivity('test'); + + $this->assertNotEmpty($activityBag->getAll()); + } + + public function testGetActivityThrowsForInvalidResult(): void + { + $rawResult = RawRowResult::ACTIVITY; + unset($rawResult[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT]); + + $this->repositoryStub->method('getActivity') + ->willReturn([$rawResult]); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + $this->factoryStub, + null, + $this->repositoryStub + ); + + $this->expectException(StoreException::class); + $store->getActivity('test'); + } +} diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php b/tests/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000000CreateJobTableTest.php similarity index 74% rename from tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php rename to tests/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000000CreateJobTableTest.php index d6624b3f291282e481153e8cd1f7d413d25802ff..b56de0e42d4ff53ea3c72c71ff6d81fa7e67fb40 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000000CreateJobTableTest.php @@ -1,21 +1,22 @@ <?php -namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; +namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations; use Doctrine\DBAL\Schema\AbstractSchemaManager; use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable; use PHPUnit\Framework\TestCase; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; /** - * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases\AbstractCreateJobsTable * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration - * */ -class Version20220601000100CreateFailedJobsTableTest extends TestCase +class Version20220601000000CreateJobTableTest extends TestCase { protected Connection $connection; protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; @@ -23,15 +24,15 @@ class Version20220601000100CreateFailedJobsTableTest extends TestCase protected function setUp(): void { - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); $this->schemaManager = $this->connection->dbal()->createSchemaManager(); - $this->tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); + $this->tableName = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); } public function testCanRunMigration(): void { $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); - $migration = new Migrations\Version20220601000100CreateFailedJobsTable($this->connection); + $migration = new Version20220601000000CreateJobTable($this->connection); $migration->run(); $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); $migration->revert(); @@ -49,7 +50,7 @@ class Version20220601000100CreateFailedJobsTableTest extends TestCase $schemaManagerStub->method('createTable') ->willThrowException(new \Doctrine\DBAL\Exception('test')); - $migration = new Migrations\Version20220601000100CreateFailedJobsTable($connectionStub); + $migration = new Version20220601000000CreateJobTable($connectionStub); $this->expectException(MigrationException::class); $migration->run(); } @@ -65,7 +66,7 @@ class Version20220601000100CreateFailedJobsTableTest extends TestCase $schemaManagerStub->method('dropTable') ->willThrowException(new \Doctrine\DBAL\Exception('test')); - $migration = new Migrations\Version20220601000100CreateFailedJobsTable($connectionStub); + $migration = new Version20220601000000CreateJobTable($connectionStub); $this->expectException(MigrationException::class); $migration->revert(); } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php b/tests/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000100CreateJobFailedTableTest.php similarity index 73% rename from tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php rename to tests/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000100CreateJobFailedTableTest.php index 011b249112ee52f246d272fed33acc107639241d..0240fc6c4d035d518df04978980ba707761477c0 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/Store/Migrations/Version20220601000100CreateJobFailedTableTest.php @@ -1,20 +1,22 @@ <?php -namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; +namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations; use Doctrine\DBAL\Schema\AbstractSchemaManager; use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations; use PHPUnit\Framework\TestCase; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; /** - * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000100CreateJobFailedTable * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases\AbstractCreateJobsTable */ -class Version20220601000000CreateJobsTableTest extends TestCase +class Version20220601000100CreateJobFailedTableTest extends TestCase { protected Connection $connection; protected \Doctrine\DBAL\Schema\AbstractSchemaManager $schemaManager; @@ -22,15 +24,15 @@ class Version20220601000000CreateJobsTableTest extends TestCase protected function setUp(): void { - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); $this->schemaManager = $this->connection->dbal()->createSchemaManager(); - $this->tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); + $this->tableName = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB_FAILED); } public function testCanRunMigration(): void { $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); - $migration = new Version20220601000000CreateJobsTable($this->connection); + $migration = new Migrations\Version20220601000100CreateJobFailedTable($this->connection); $migration->run(); $this->assertTrue($this->schemaManager->tablesExist($this->tableName)); $migration->revert(); @@ -48,7 +50,7 @@ class Version20220601000000CreateJobsTableTest extends TestCase $schemaManagerStub->method('createTable') ->willThrowException(new \Doctrine\DBAL\Exception('test')); - $migration = new Version20220601000000CreateJobsTable($connectionStub); + $migration = new Migrations\Version20220601000100CreateJobFailedTable($connectionStub); $this->expectException(MigrationException::class); $migration->run(); } @@ -64,7 +66,7 @@ class Version20220601000000CreateJobsTableTest extends TestCase $schemaManagerStub->method('dropTable') ->willThrowException(new \Doctrine\DBAL\Exception('test')); - $migration = new Version20220601000000CreateJobsTable($connectionStub); + $migration = new Migrations\Version20220601000100CreateJobFailedTable($connectionStub); $this->expectException(MigrationException::class); $migration->revert(); } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/RawJobTest.php b/tests/src/Stores/Jobs/DoctrineDbal/Store/RawJobTest.php similarity index 58% rename from tests/src/Stores/Jobs/DoctrineDbal/JobsStore/RawJobTest.php rename to tests/src/Stores/Jobs/DoctrineDbal/Store/RawJobTest.php index 7615243deec2246d0de11e98973c8ff355ae8bff..65b9ab9e940fffb0fd4a0806829cbbf71f026742 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/RawJobTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/Store/RawJobTest.php @@ -1,51 +1,56 @@ <?php -namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\Store; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\SqlitePlatform; use PHPUnit\Framework\TestCase; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\RawJob; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\RawJob; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; /** - * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\RawJob - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\RawJob + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper */ class RawJobTest extends TestCase { - protected AuthenticationEvent $authenticationEvent; + protected Event $authenticationEvent; protected array $validRawRow; protected \PHPUnit\Framework\MockObject\Stub $abstractPlatformStub; protected function setUp(): void { $this->abstractPlatformStub = $this->createStub(AbstractPlatform::class); - $this->authenticationEvent = new AuthenticationEvent(['sample' => 'state']); + $this->authenticationEvent = new Event(new State(StateArrays::FULL)); $this->validRawRow = [ - JobsStore::COLUMN_NAME_ID => 1, - JobsStore::COLUMN_NAME_PAYLOAD => serialize($this->authenticationEvent), - JobsStore::COLUMN_NAME_TYPE => get_class($this->authenticationEvent), - JobsStore::COLUMN_NAME_CREATED_AT => '2022-08-17 13:26:12', + Store\TableConstants::COLUMN_NAME_ID => 1, + Store\TableConstants::COLUMN_NAME_PAYLOAD => serialize($this->authenticationEvent), + Store\TableConstants::COLUMN_NAME_TYPE => get_class($this->authenticationEvent), + Store\TableConstants::COLUMN_NAME_CREATED_AT => '2022-08-17 13:26:12', ]; } public function testCanInstantiateValidRawJob(): void { $abstractPlatform = new SqlitePlatform(); - $rawJob = new JobsStore\RawJob($this->validRawRow, $abstractPlatform); - $this->assertSame($rawJob->getId(), $this->validRawRow[JobsStore::COLUMN_NAME_ID]); + $rawJob = new Store\RawJob($this->validRawRow, $abstractPlatform); + $this->assertSame($rawJob->getId(), $this->validRawRow[Store\TableConstants::COLUMN_NAME_ID]); $this->assertEquals($rawJob->getPayload(), $this->authenticationEvent); - $this->assertSame($rawJob->getType(), $this->validRawRow[JobsStore::COLUMN_NAME_TYPE]); + $this->assertSame($rawJob->getType(), $this->validRawRow[Store\TableConstants::COLUMN_NAME_TYPE]); $this->assertInstanceOf(\DateTimeImmutable::class, $rawJob->getCreatedAt()); } public function testThrowsOnEmptyColumn(): void { $invalidRawRow = $this->validRawRow; - unset($invalidRawRow[JobsStore::COLUMN_NAME_ID]); + unset($invalidRawRow[Store\TableConstants::COLUMN_NAME_ID]); $this->expectException(UnexpectedValueException::class); @@ -56,7 +61,7 @@ class RawJobTest extends TestCase public function testThrowsOnNonNumericId(): void { $invalidRawRow = $this->validRawRow; - $invalidRawRow[JobsStore::COLUMN_NAME_ID] = 'a'; + $invalidRawRow[Store\TableConstants::COLUMN_NAME_ID] = 'a'; $this->expectException(UnexpectedValueException::class); @@ -67,7 +72,7 @@ class RawJobTest extends TestCase public function testThrowsOnNonStringPayload(): void { $invalidRawRow = $this->validRawRow; - $invalidRawRow[JobsStore::COLUMN_NAME_PAYLOAD] = 123; + $invalidRawRow[Store\TableConstants::COLUMN_NAME_PAYLOAD] = 123; $this->expectException(UnexpectedValueException::class); @@ -78,29 +83,29 @@ class RawJobTest extends TestCase public function testThrowsOnNonAbstractPayload(): void { $invalidRawRow = $this->validRawRow; - $invalidRawRow[JobsStore::COLUMN_NAME_PAYLOAD] = serialize('abc'); + $invalidRawRow[Store\TableConstants::COLUMN_NAME_PAYLOAD] = serialize('abc'); $this->expectException(UnexpectedValueException::class); /** @psalm-suppress InvalidArgument */ - new JobsStore\RawJob($invalidRawRow, $this->abstractPlatformStub); + new Store\RawJob($invalidRawRow, $this->abstractPlatformStub); } public function testThrowsOnNonStringType(): void { $invalidRawRow = $this->validRawRow; - $invalidRawRow[JobsStore::COLUMN_NAME_TYPE] = 123; + $invalidRawRow[Store\TableConstants::COLUMN_NAME_TYPE] = 123; $this->expectException(UnexpectedValueException::class); /** @psalm-suppress InvalidArgument */ - new JobsStore\RawJob($invalidRawRow, $this->abstractPlatformStub); + new Store\RawJob($invalidRawRow, $this->abstractPlatformStub); } public function testThrowsOnNonStringCreatedAt(): void { $invalidRawRow = $this->validRawRow; - $invalidRawRow[JobsStore::COLUMN_NAME_CREATED_AT] = 123; + $invalidRawRow[Store\TableConstants::COLUMN_NAME_CREATED_AT] = 123; $this->expectException(UnexpectedValueException::class); @@ -111,11 +116,11 @@ class RawJobTest extends TestCase public function testThrowsOnNonValidCreatedAt(): void { $invalidRawRow = $this->validRawRow; - $invalidRawRow[JobsStore::COLUMN_NAME_CREATED_AT] = '123'; + $invalidRawRow[Store\TableConstants::COLUMN_NAME_CREATED_AT] = '123'; $this->expectException(UnexpectedValueException::class); /** @psalm-suppress InvalidArgument */ - new JobsStore\RawJob($invalidRawRow, $this->abstractPlatformStub); + new Store\RawJob($invalidRawRow, $this->abstractPlatformStub); } } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/RepositoryTest.php b/tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php similarity index 81% rename from tests/src/Stores/Jobs/DoctrineDbal/JobsStore/RepositoryTest.php rename to tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php index 11c09baa0fce5395a128a5e439977df5890926ad..fba787b148762a3f2079904f691b54ae3e4cfc72 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/RepositoryTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php @@ -1,24 +1,28 @@ <?php -namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +declare(strict_types=1); -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\Store; + +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; use SimpleSAML\Module\accounting\Entities\GenericJob; use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Services\Logger; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Repository; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpKernel\HttpCache\Store; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; /** - * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Repository + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper @@ -26,12 +30,17 @@ use Symfony\Component\HttpKernel\HttpCache\Store; * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\RawJob - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000100CreateJobFailedTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\RawJob + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event\Job + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases\AbstractCreateJobsTable + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper */ class RepositoryTest extends TestCase { @@ -42,16 +51,16 @@ class RepositoryTest extends TestCase protected \PHPUnit\Framework\MockObject\Stub $factoryStub; protected \PHPUnit\Framework\MockObject\Stub $payloadStub; protected \PHPUnit\Framework\MockObject\Stub $jobStub; - protected JobsStore $jobsStore; + protected Store $jobsStore; protected string $jobsTableName; protected function setUp(): void { // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml). $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php'); - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); - $this->loggerServiceStub = $this->createStub(LoggerService::class); + $this->loggerServiceStub = $this->createStub(Logger::class); /** @psalm-suppress InvalidArgument */ $this->migrator = new Migrator($this->connection, $this->loggerServiceStub); @@ -67,9 +76,9 @@ class RepositoryTest extends TestCase $this->jobStub->method('getCreatedAt')->willReturn(new \DateTimeImmutable()); /** @psalm-suppress InvalidArgument */ - $this->jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $this->jobsStore = new Store($this->moduleConfiguration, $this->loggerServiceStub, $this->factoryStub); - $this->jobsTableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); + $this->jobsTableName = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); } public function testCanInsertAndGetJob(): void @@ -109,7 +118,7 @@ class RepositoryTest extends TestCase $this->expectException(StoreException::class); - $invalidType = str_pad('abc', JobsStore::COLUMN_LENGTH_TYPE + 1); + $invalidType = str_pad('abc', Store\TableConstants::COLUMN_TYPE_LENGTH + 1); $jobStub = $this->createStub(GenericJob::class); $jobStub->method('getPayload')->willReturn($this->payloadStub); $jobStub->method('getType')->willReturn($invalidType); @@ -198,12 +207,12 @@ class RepositoryTest extends TestCase /** @psalm-suppress InvalidArgument */ $repository->insert($this->jobStub); - $authenticationEvent = new AuthenticationEvent(['sample-state']); - $authenticationEventJob = new AuthenticationEvent\Job($authenticationEvent); + $authenticationEvent = new Event(new State(StateArrays::FULL)); + $authenticationEventJob = new Event\Job($authenticationEvent); $repository->insert($authenticationEventJob); - $this->assertInstanceOf(AuthenticationEvent\Job::class, $repository->getNext(AuthenticationEvent\Job::class)); + $this->assertInstanceOf(Event\Job::class, $repository->getNext(Event\Job::class)); } public function testInitializationThrowsForInvalidJobsTableName(): void diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php b/tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php similarity index 72% rename from tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php rename to tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php index d7c5a7db520e571d5a50f93de12e1e02f8540377..4949b3548f649e406ad3987fb215df8986350ea9 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php @@ -1,22 +1,28 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal; use PHPUnit\Framework\TestCase; -use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\State; use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; use SimpleSAML\Module\accounting\Entities\GenericJob; use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Services\Logger; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; -use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; /** - * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store + * @covers \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore * @uses \SimpleSAML\Module\accounting\ModuleConfiguration * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory @@ -24,20 +30,24 @@ use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000000CreateJobTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Version20220601000100CreateJobFailedTable * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\RawJob - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent - * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job - * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Repository + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\RawJob + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Event\Job + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Migrations\Bases\AbstractCreateJobsTable + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\State + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity + * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper */ -class JobsStoreTest extends TestCase +class StoreTest extends TestCase { protected ModuleConfiguration $moduleConfiguration; protected \PHPUnit\Framework\MockObject\Stub $factoryStub; protected Connection $connection; - protected \PHPUnit\Framework\MockObject\Stub $loggerServiceStub; + protected \PHPUnit\Framework\MockObject\Stub $loggerStub; protected Migrator $migrator; protected \PHPUnit\Framework\MockObject\Stub $payloadStub; protected \PHPUnit\Framework\MockObject\Stub $jobStub; @@ -46,12 +56,12 @@ class JobsStoreTest extends TestCase { // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml). $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php'); - $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + $this->connection = new Connection(ConnectionParameters::DBAL_SQLITE_MEMORY); - $this->loggerServiceStub = $this->createStub(LoggerService::class); + $this->loggerStub = $this->createStub(Logger::class); /** @psalm-suppress InvalidArgument */ - $this->migrator = new Migrator($this->connection, $this->loggerServiceStub); + $this->migrator = new Migrator($this->connection, $this->loggerStub); $this->factoryStub = $this->createStub(Factory::class); $this->factoryStub->method('buildConnection')->willReturn($this->connection); @@ -68,7 +78,7 @@ class JobsStoreTest extends TestCase public function testSetupDependsOnMigratorSetup(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); $this->assertTrue($this->migrator->needsSetup()); $this->assertTrue($jobsStore->needsSetup()); @@ -82,9 +92,9 @@ class JobsStoreTest extends TestCase public function testSetupDependsOnMigrations(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); - // Run migrator setup beforehand, so it only depends on JobsStore migrations setup + // Run migrator setup beforehand, so it only depends on Store migrations setup $this->migrator->runSetup(); $this->assertTrue($jobsStore->needsSetup()); @@ -96,23 +106,34 @@ class JobsStoreTest extends TestCase public function testCanGetPrefixedTableNames(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); - $tableNameJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); - $tableNameFailedJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); + $tableNameJobs = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); + $tableNameFailedJobs = $this->connection->preparePrefixedTableName( + Store\TableConstants::TABLE_NAME_JOB_FAILED + ); $this->assertSame($tableNameJobs, $jobsStore->getPrefixedTableNameJobs()); $this->assertSame($tableNameFailedJobs, $jobsStore->getPrefixedTableNameFailedJobs()); } + public function testCanBuildInstanceStatically(): void + { + $moduleConfiguration = $this->createStub(ModuleConfiguration::class); + $moduleConfiguration->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + /** @psalm-suppress InvalidArgument */ + $this->assertInstanceOf(Store::class, Store::build($moduleConfiguration, $this->loggerStub)); + } + public function testCanEnqueueJob(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); $jobsStore->runSetup(); $queryBuilder = $this->connection->dbal()->createQueryBuilder(); - $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne(); + $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs()); $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne()); @@ -132,7 +153,7 @@ class JobsStoreTest extends TestCase public function testEnqueueThrowsStoreExceptionOnNonSetupRun(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); // Don't run setup, so we get exception //$jobsStore->runSetup(); @@ -148,7 +169,7 @@ class JobsStoreTest extends TestCase public function testCanDequeueJob(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); $jobsStore->runSetup(); $queryBuilder = $this->connection->dbal()->createQueryBuilder(); @@ -171,11 +192,11 @@ class JobsStoreTest extends TestCase public function testCanDequeueSpecificJobType(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); $jobsStore->runSetup(); - $authenticationEvent = new AuthenticationEvent(['sample-state']); - $authenticationEventJob = new AuthenticationEvent\Job($authenticationEvent); + $authenticationEvent = new Event(new State(StateArrays::FULL)); + $authenticationEventJob = new Event\Job($authenticationEvent); $queryBuilder = $this->connection->dbal()->createQueryBuilder(); $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne(); @@ -188,11 +209,11 @@ class JobsStoreTest extends TestCase $this->assertSame(2, (int) $queryBuilder->executeQuery()->fetchOne()); - $this->assertInstanceOf(AuthenticationEvent\Job::class, $jobsStore->dequeue(AuthenticationEvent\Job::class)); + $this->assertInstanceOf(Event\Job::class, $jobsStore->dequeue(Event\Job::class)); $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); - $this->assertNull($jobsStore->dequeue(AuthenticationEvent::class)); + $this->assertNull($jobsStore->dequeue(Event::class)); $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); } @@ -200,7 +221,7 @@ class JobsStoreTest extends TestCase public function testDequeueThrowsWhenSetupNotRun(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); // $jobsStore->runSetup(); $payloadStub = $this->createStub(AbstractPayload::class); @@ -214,7 +235,7 @@ class JobsStoreTest extends TestCase public function testDequeueThrowsForJobWithInvalidId(): void { - $repositoryStub = $this->createStub(JobsStore\Repository::class); + $repositoryStub = $this->createStub(Store\Repository::class); $jobStub = $this->createStub(GenericJob::class); $jobStub->method('getPayload')->willReturn($this->payloadStub); $jobStub->method('getCreatedAt')->willReturn(new \DateTimeImmutable()); @@ -224,10 +245,11 @@ class JobsStoreTest extends TestCase $repositoryStub->method('getNext')->willReturn($jobStub); /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore( + $jobsStore = new Store( $this->moduleConfiguration, + $this->loggerStub, $this->factoryStub, - $this->loggerServiceStub, + null, $repositoryStub ); $jobsStore->runSetup(); @@ -239,15 +261,16 @@ class JobsStoreTest extends TestCase public function testDequeThrowsAfterMaxDeleteAttempts(): void { - $repositoryStub = $this->createStub(JobsStore\Repository::class); + $repositoryStub = $this->createStub(Store\Repository::class); $repositoryStub->method('getNext')->willReturn($this->jobStub); $repositoryStub->method('delete')->willReturn(false); /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore( + $jobsStore = new Store( $this->moduleConfiguration, + $this->loggerStub, $this->factoryStub, - $this->loggerServiceStub, + null, $repositoryStub ); $jobsStore->runSetup(); @@ -259,15 +282,16 @@ class JobsStoreTest extends TestCase public function testCanContinueSearchingInCaseOfJobDeletion(): void { - $repositoryStub = $this->createStub(JobsStore\Repository::class); + $repositoryStub = $this->createStub(Store\Repository::class); $repositoryStub->method('getNext')->willReturn($this->jobStub); $repositoryStub->method('delete')->willReturnOnConsecutiveCalls(false, true); /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore( + $jobsStore = new Store( $this->moduleConfiguration, + $this->loggerStub, $this->factoryStub, - $this->loggerServiceStub, + null, $repositoryStub ); $jobsStore->runSetup(); diff --git a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..00776a3cf5c01283cfb8a046e139d8a519d2026a --- /dev/null +++ b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php @@ -0,0 +1,138 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Activity; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; +use SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker + * @uses \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper + * @uses \SimpleSAML\Module\accounting\Stores\Builders\Bases\AbstractStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Builders\DataStoreBuilder + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractStore + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store + * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository + * @uses \SimpleSAML\Module\accounting\Helpers\HashHelper + * + * @psalm-suppress all + */ +class TrackerTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub|ModuleConfiguration|ModuleConfiguration&\PHPUnit\Framework\MockObject\Stub + */ + protected $moduleConfigurationStub; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface|LoggerInterface&\PHPUnit\Framework\MockObject\MockObject + */ + protected $loggerMock; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|Store|Store&\PHPUnit\Framework\MockObject\MockObject + */ + protected $dataStoreMock; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->dataStoreMock = $this->createMock(Store::class); + } + + /** + * @psalm-suppress PossiblyInvalidArgument + */ + public function testCanCreateInstance(): void + { + $this->assertInstanceOf( + Tracker::class, + new Tracker($this->moduleConfigurationStub, $this->loggerMock, $this->dataStoreMock) + ); + + $this->assertInstanceOf( + Tracker::class, + new Tracker($this->moduleConfigurationStub, $this->loggerMock) + ); + + $this->assertInstanceOf( + Tracker::class, + Tracker::build($this->moduleConfigurationStub, $this->loggerMock) + ); + } + + public function testProcessCallsPersistOnDataStore(): void + { + $authenticationEventStub = $this->createStub(Event::class); + + $this->dataStoreMock->expects($this->once()) + ->method('persist') + ->with($authenticationEventStub); + + /** @psalm-suppress PossiblyInvalidArgument */ + $tracker = new Tracker($this->moduleConfigurationStub, $this->loggerMock, $this->dataStoreMock); + + $tracker->process($authenticationEventStub); + } + + public function testSetupDependsOnDataStore(): void + { + $this->dataStoreMock->expects($this->once()) + ->method('needsSetup') + ->willReturn(true); + + $this->dataStoreMock->expects($this->once()) + ->method('runSetup'); + + /** @psalm-suppress PossiblyInvalidArgument */ + $tracker = new Tracker($this->moduleConfigurationStub, $this->loggerMock, $this->dataStoreMock); + + $this->assertTrue($tracker->needsSetup()); + + $tracker->runSetup(); + } + + public function testGetConnectedServiceProviders(): void + { + $connectedOrganizationsBagStub = $this->createStub(ConnectedServiceProvider\Bag::class); + $this->dataStoreMock->expects($this->once()) + ->method('getConnectedOrganizations') + ->willReturn($connectedOrganizationsBagStub); + + /** @psalm-suppress PossiblyInvalidArgument */ + $tracker = new Tracker($this->moduleConfigurationStub, $this->loggerMock, $this->dataStoreMock); + + $this->assertInstanceOf( + ConnectedServiceProvider\Bag::class, + $tracker->getConnectedServiceProviders('test') + ); + } + + public function testGetActivity(): void + { + $activityBag = $this->createStub(Activity\Bag::class); + $this->dataStoreMock->expects($this->once()) + ->method('getActivity') + ->willReturn($activityBag); + + $tracker = new Tracker($this->moduleConfigurationStub, $this->loggerMock, $this->dataStoreMock); + + $this->assertInstanceOf( + Activity\Bag::class, + $tracker->getActivity('test') + ); + } +} diff --git a/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php b/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6fd033f2cede8c4c430ee49c18c4dcd2d0a96003 --- /dev/null +++ b/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php @@ -0,0 +1,96 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Trackers\Builders; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Exceptions\Exception; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Trackers\Interfaces\AuthenticationDataTrackerInterface; +use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; + +/** + * @covers \SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder + * @uses \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper + * + * @psalm-suppress all + */ +class AuthenticationDataTrackerBuilderTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface|LoggerInterface&\PHPUnit\Framework\MockObject\MockObject + */ + protected $loggerMock; + /** + * @var \PHPUnit\Framework\MockObject\Stub|ModuleConfiguration|ModuleConfiguration&\PHPUnit\Framework\MockObject\Stub + */ + protected $moduleConfigurationStub; + + protected AuthenticationDataTrackerInterface $trackerStub; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(ConnectionParameters::DBAL_SQLITE_MEMORY); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->trackerStub = new class implements AuthenticationDataTrackerInterface { + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger + ): AuthenticationDataTrackerInterface { + return new self(); + } + + public function process(Event $authenticationEvent): void + { + } + + public function needsSetup(): bool + { + return false; + } + + public function runSetup(): void + { + } + }; + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf( + AuthenticationDataTrackerBuilder::class, + new AuthenticationDataTrackerBuilder($this->moduleConfigurationStub, $this->loggerMock) + ); + } + + public function testCanBuildAuthenticationDataTracker(): void + { + $authenticationDataTrackerBuilder = new AuthenticationDataTrackerBuilder( + $this->moduleConfigurationStub, + $this->loggerMock + ); + + $trackerClass = get_class($this->trackerStub); + + $this->assertInstanceOf($trackerClass, $authenticationDataTrackerBuilder->build($trackerClass)); + } + + public function testBuildThrowsForInvalidTrackerClass(): void + { + $authenticationDataTrackerBuilder = new AuthenticationDataTrackerBuilder( + $this->moduleConfigurationStub, + $this->loggerMock + ); + + $this->expectException(Exception::class); + + $authenticationDataTrackerBuilder->build('invalid'); + } +}