From 82e4023415ea0eda72ae61a69286f5c78f228ce9 Mon Sep 17 00:00:00 2001 From: Eric Richer Date: Thu, 23 Apr 2026 10:31:41 -0400 Subject: [PATCH] Added tests. Fixed issues from test. Signed-off-by: Eric Richer --- composer.json | 4 +- composer.lock | 339 ++++++++++---------- psalm.baseline.xml | 14 +- src/ApiProblem.php | 3 - src/ApiProblemResponse.php | 4 +- src/Exception/DomainException.php | 53 +++ src/Exception/ProblemExceptionInterface.php | 4 +- test/ApiProblemResponseTest.php | 64 ++++ test/ApiProblemTest.php | 269 ++++++++++++++++ test/Exception/DomainExceptionTest.php | 25 ++ 10 files changed, 596 insertions(+), 183 deletions(-) create mode 100644 src/Exception/DomainException.php create mode 100644 test/ApiProblemResponseTest.php create mode 100644 test/Exception/DomainExceptionTest.php diff --git a/composer.json b/composer.json index 0172888..0fb807e 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ }, "require-dev": { "laminas/laminas-coding-standard": "^3.1", - "phpunit/phpunit": "^12.5", + "phpunit/phpunit": "^12.5.22", "psalm/plugin-phpunit": "^0.19.5", - "vimeo/psalm": "^6.14" + "vimeo/psalm": "^6.16" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 70bffe3..65e3fb1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "56ac9a49b9715ab2cb0b30a34a38fa3e", + "content-hash": "3520ccb77ff9de0a1a6ac2a8567a13e6", "packages": [ { "name": "laminas/laminas-diactoros", @@ -797,24 +797,27 @@ }, { "name": "amphp/serialization", - "version": "v1.0.0", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -849,22 +852,28 @@ ], "support": { "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" + "source": "https://github.com/amphp/serialization/tree/v1.1.0" }, - "time": "2020-03-25T21:39:07+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" }, { "name": "amphp/socket", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", "shasum": "" }, "require": { @@ -873,17 +882,17 @@ "amphp/dns": "^2", "ext-openssl": "*", "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", + "league/uri": "^7", + "league/uri-interfaces": "^7", "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" + "revolt/event-loop": "^1" }, "require-dev": { "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "amphp/process": "^2", "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -927,7 +936,7 @@ ], "support": { "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" + "source": "https://github.com/amphp/socket/tree/v2.4.0" }, "funding": [ { @@ -935,7 +944,7 @@ "type": "github" } ], - "time": "2024-04-21T14:33:03+00:00" + "time": "2026-04-19T15:09:56+00:00" }, { "name": "amphp/sync", @@ -1465,29 +1474,29 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.5", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -1507,9 +1516,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "felixfbecker/language-server-protocol", @@ -1741,20 +1750,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -1827,7 +1836,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -1835,20 +1844,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -1911,7 +1920,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -1919,7 +1928,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "myclabs/deep-copy", @@ -1983,16 +1992,16 @@ }, { "name": "netresearch/jsonmapper", - "version": "v5.0.0", + "version": "v5.0.1", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" + "reference": "980674efdda65913492d29a8fd51c82270dd37bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", - "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/980674efdda65913492d29a8fd51c82270dd37bb", + "reference": "980674efdda65913492d29a8fd51c82270dd37bb", "shasum": "" }, "require": { @@ -2028,9 +2037,9 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.1" }, - "time": "2024-09-08T10:20:00+00:00" + "time": "2026-02-22T16:28:03+00:00" }, { "name": "nikic/php-parser", @@ -2263,16 +2272,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.1", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", - "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -2322,9 +2331,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-01-20T15:30:42+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2433,16 +2442,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.2", + "version": "12.5.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" + "reference": "876099a072646c7745f673d7aeab5382c4439691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", - "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", + "reference": "876099a072646c7745f673d7aeab5382c4439691", "shasum": "" }, "require": { @@ -2451,7 +2460,6 @@ "ext-xmlwriter": "*", "nikic/php-parser": "^5.7.0", "php": ">=8.3", - "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", "sebastian/environment": "^8.0.3", @@ -2498,7 +2506,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" }, "funding": [ { @@ -2518,20 +2526,20 @@ "type": "tidelift" } ], - "time": "2025-12-24T07:03:04+00:00" + "time": "2026-04-15T08:23:17+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", - "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { @@ -2571,15 +2579,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:37+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", @@ -2767,16 +2787,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.8", + "version": "12.5.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889" + "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/37ddb96c14bfee10304825edbb7e66d341ec6889", - "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", + "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", "shasum": "" }, "require": { @@ -2790,18 +2810,19 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.2", - "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-code-coverage": "^12.5.6", + "phpunit/php-file-iterator": "^6.0.1", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.4", + "sebastian/comparator": "^7.1.6", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.3", + "sebastian/environment": "^8.1.0", "sebastian/exporter": "^7.0.2", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", "sebastian/type": "^6.0.3", "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" @@ -2844,35 +2865,19 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-01-27T06:12:29+00:00" + "time": "2026-04-18T06:12:49+00:00" }, { "name": "psalm/plugin-phpunit", - "version": "0.19.5", + "version": "0.19.7", "source": { "type": "git", "url": "https://github.com/psalm/psalm-plugin-phpunit.git", @@ -2924,7 +2929,7 @@ "description": "Psalm plugin for PHPUnit", "support": { "issues": "https://github.com/psalm/psalm-plugin-phpunit/issues", - "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.19.5" + "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.19.7" }, "time": "2025-03-31T18:49:55+00:00" }, @@ -3174,16 +3179,16 @@ }, { "name": "sebastian/comparator", - "version": "7.1.4", + "version": "7.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", "shasum": "" }, "require": { @@ -3242,7 +3247,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" }, "funding": [ { @@ -3262,7 +3267,7 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:28:48+00:00" + "time": "2026-04-14T08:23:15+00:00" }, { "name": "sebastian/complexity", @@ -3391,16 +3396,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.3", + "version": "8.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", "shasum": "" }, "require": { @@ -3415,7 +3420,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -3443,7 +3448,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" }, "funding": [ { @@ -3463,7 +3468,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2026-04-15T12:13:01+00:00" }, { "name": "sebastian/exporter", @@ -4266,16 +4271,16 @@ }, { "name": "symfony/console", - "version": "v7.4.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", "shasum": "" }, "require": { @@ -4340,7 +4345,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.4" + "source": "https://github.com/symfony/console/tree/v7.4.8" }, "funding": [ { @@ -4360,7 +4365,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T11:36:38+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4431,16 +4436,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", "shasum": "" }, "require": { @@ -4477,7 +4482,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.8" }, "funding": [ { @@ -4497,20 +4502,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -4560,7 +4565,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" }, "funding": [ { @@ -4580,20 +4585,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", "shasum": "" }, "require": { @@ -4642,7 +4647,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" }, "funding": [ { @@ -4662,11 +4667,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4727,7 +4732,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" }, "funding": [ { @@ -4751,16 +4756,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -4812,7 +4817,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" }, "funding": [ { @@ -4832,20 +4837,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { @@ -4892,7 +4897,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0" }, "funding": [ { @@ -4912,7 +4917,7 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { "name": "symfony/service-contracts", @@ -5003,16 +5008,16 @@ }, { "name": "symfony/string", - "version": "v7.4.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" + "reference": "114ac57257d75df748eda23dd003878080b8e688" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", "shasum": "" }, "require": { @@ -5070,7 +5075,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.4" + "source": "https://github.com/symfony/string/tree/v7.4.8" }, "funding": [ { @@ -5090,7 +5095,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T10:54:30+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "theseer/tokenizer", @@ -5144,16 +5149,16 @@ }, { "name": "vimeo/psalm", - "version": "6.14.3", + "version": "6.16.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d0b040a91f280f071c1abcb1b77ce3822058725a" + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d0b040a91f280f071c1abcb1b77ce3822058725a", - "reference": "d0b040a91f280f071c1abcb1b77ce3822058725a", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", "shasum": "" }, "require": { @@ -5177,7 +5182,7 @@ "netresearch/jsonmapper": "^5.0", "nikic/php-parser": "^5.0.0", "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^6.0 || ^7.0 || ^8.0", "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3 || ^8.0", @@ -5258,7 +5263,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2025-12-23T15:36:48+00:00" + "time": "2026-03-19T10:56:09+00:00" }, { "name": "webimpress/coding-standard", @@ -5317,16 +5322,16 @@ }, { "name": "webmozart/assert", - "version": "2.1.2", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", - "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "shasum": "" }, "require": { @@ -5373,9 +5378,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.2" + "source": "https://github.com/webmozarts/assert/tree/2.3.0" }, - "time": "2026-01-13T14:02:24+00:00" + "time": "2026-04-11T10:33:05+00:00" } ], "aliases": [], diff --git a/psalm.baseline.xml b/psalm.baseline.xml index 1e6e463..8322eb5 100644 --- a/psalm.baseline.xml +++ b/psalm.baseline.xml @@ -1,5 +1,5 @@ - + @@ -10,10 +10,6 @@ - - - - @@ -25,8 +21,10 @@ - - - + + + + + diff --git a/src/ApiProblem.php b/src/ApiProblem.php index 66309af..82bf05b 100644 --- a/src/ApiProblem.php +++ b/src/ApiProblem.php @@ -271,9 +271,6 @@ protected function getTitle(): ?string return get_class($this->detail); } - if (null === $this->title) { - return 'Unknown'; - } return 'Unknown'; } diff --git a/src/ApiProblemResponse.php b/src/ApiProblemResponse.php index 4b3f5cf..32beab8 100644 --- a/src/ApiProblemResponse.php +++ b/src/ApiProblemResponse.php @@ -18,6 +18,7 @@ use const JSON_HEX_APOS; use const JSON_HEX_QUOT; use const JSON_HEX_TAG; +use const JSON_PARTIAL_OUTPUT_ON_ERROR; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; @@ -32,7 +33,8 @@ final class ApiProblemResponse extends Response | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT - | JSON_UNESCAPED_SLASHES; + | JSON_UNESCAPED_SLASHES + | JSON_PARTIAL_OUTPUT_ON_ERROR; public function __construct(ApiProblem $apiProblem) { diff --git a/src/Exception/DomainException.php b/src/Exception/DomainException.php new file mode 100644 index 0000000..0ffd5ee --- /dev/null +++ b/src/Exception/DomainException.php @@ -0,0 +1,53 @@ +details = $details; + return $this; + } + + public function setType(string $uri): self + { + $this->type = $uri; + return $this; + } + + public function setTitle(string $title): self + { + $this->title = $title; + return $this; + } + + #[Override] + public function getAdditionalDetails(): Traversable|array|null + { + return $this->details; + } + + #[Override] + public function getType(): ?string + { + return $this->type; + } + + #[Override] + public function getTitle(): ?string + { + return $this->title; + } +} diff --git a/src/Exception/ProblemExceptionInterface.php b/src/Exception/ProblemExceptionInterface.php index 0bd31f4..b6edad7 100644 --- a/src/Exception/ProblemExceptionInterface.php +++ b/src/Exception/ProblemExceptionInterface.php @@ -13,7 +13,7 @@ interface ProblemExceptionInterface { public function getAdditionalDetails(): Traversable|array|null; - public function getType(): string; + public function getType(): ?string; - public function getTitle(): string; + public function getTitle(): ?string; } diff --git a/test/ApiProblemResponseTest.php b/test/ApiProblemResponseTest.php new file mode 100644 index 0000000..fad2647 --- /dev/null +++ b/test/ApiProblemResponseTest.php @@ -0,0 +1,64 @@ +assertEquals(400, $response->getStatusCode()); + $this->assertIsString($response->getReasonPhrase()); + $this->assertNotEmpty($response->getReasonPhrase()); + $this->assertEquals('bad request', strtolower($response->getReasonPhrase())); + } + + public function testApiProblemResponseSetsStatusCodeAndReasonPhraseUsingException(): void + { + $exception = new Exception\DomainException('Random error', 400); + $response = new ApiProblemResponse(new ApiProblem(400, $exception)); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertIsString($response->getReasonPhrase()); + $this->assertNotEmpty($response->getReasonPhrase()); + $this->assertEquals('bad request', strtolower($response->getReasonPhrase())); + } + + public function testApiProblemResponseBodyIsSerializedApiProblem(): void + { + $additional = [ + 'foo' => fopen('php://memory', 'r'), + ]; + + $expected = [ + 'foo' => null, + 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html', + 'title' => 'Bad Request', + 'status' => 400, + 'detail' => 'Random error', + ]; + + $apiProblem = new ApiProblem(400, 'Random error', null, null, $additional); + $response = new ApiProblemResponse($apiProblem); + $this->assertEquals($expected, json_decode($response->getBody()->getContents(), true)); + } + + public function testApiProblemResponseSetsContentTypeHeader(): void + { + $response = new ApiProblemResponse(new ApiProblem(400, 'Random error')); + $headers = $response->getHeaders(); + $this->assertArrayHasKey('content-type', $headers); + $header = $headers['content-type'][0]; + $this->assertEquals(ApiProblem::CONTENT_TYPE, $header); + } +} diff --git a/test/ApiProblemTest.php b/test/ApiProblemTest.php index 011e1d7..25146f9 100644 --- a/test/ApiProblemTest.php +++ b/test/ApiProblemTest.php @@ -5,13 +5,282 @@ namespace LmcTest\Api\Problem; use Lmc\Api\Problem\ApiProblem; +use Lmc\Api\Problem\Exception; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use ReflectionObject; +#[CoversClass(ApiProblem::class)] final class ApiProblemTest extends TestCase { + /** @psalm-return array */ + public static function statusCodesProvider(): array + { + return [ + '200' => [200], + '201' => [201], + '300' => [300], + '301' => [301], + '302' => [302], + '400' => [400], + '401' => [401], + '404' => [404], + '500' => [500], + ]; + } + + #[DataProvider('statusCodesProvider')] + public function testStatusIsUsedVerbatim(int $status): void + { + $apiProblem = new ApiProblem($status, 'foo'); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('status', $payload); + $this->assertEquals($status, $payload['status']); + } + public function testApiProblem(): void { $problem = new ApiProblem(400, 'Bad Request'); $this->assertEquals(400, $problem->getStatus()); } + + public function testInvalidHttpStatusCode(): void + { + $problem = new ApiProblem(99, 'Bad Request'); + $this->assertEquals(500, $problem->getStatus()); + $problem = new ApiProblem(600, 'Bad Request'); + $this->assertEquals(500, $problem->getStatus()); + } + + public function testGet(): void + { + $problem = new ApiProblem( + 400, + 'Bad Request', + 'type ref', + 'foo', + [ + 'foo' => 'bar', + ], + ); + /** @psalm-suppress InaccessibleProperty */ + $this->assertEquals('foo', $problem->title); + /** @psalm-suppress InaccessibleProperty */ + $this->assertEquals(400, $problem->status); + /** @psalm-suppress InaccessibleProperty */ + $this->assertEquals('Bad Request', $problem->detail); + /** @psalm-suppress InaccessibleProperty */ + $this->assertEquals('type ref', $problem->type); + /** @psalm-suppress UndefinedMagicPropertyFetch */ + $this->assertEquals('bar', $problem->foo); + /** @psalm-suppress UndefinedMagicPropertyFetch */ + $this->assertEquals('bar', $problem->FOO); + // test invalid + $this->expectException(Exception\InvalidArgumentException::class); + /** + * @psalm-suppress UndefinedMagicPropertyFetch + * @psalm-suppress MixedAssignment + */ + $a = $problem->badProperty; + } + + public function testExceptionCodeIsUsedForStatus(): void + { + $exception = new \Exception('exception message', 401); + $apiProblem = new ApiProblem('500', $exception); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('status', $payload); + $this->assertEquals($exception->getCode(), $payload['status']); + } + + public function testDetailStringIsUsedVerbatim(): void + { + $apiProblem = new ApiProblem('500', 'foo'); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('detail', $payload); + $this->assertEquals('foo', $payload['detail']); + } + + public function testExceptionMessageIsUsedForDetail(): void + { + $exception = new \Exception('exception message'); + $apiProblem = new ApiProblem('500', $exception); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('detail', $payload); + $this->assertEquals($exception->getMessage(), $payload['detail']); + } + + public function testExceptionsCanTriggerInclusionOfStackTraceInDetails(): void + { + $exception = new \Exception('exception message'); + $apiProblem = new ApiProblem('500', $exception); + $apiProblem->setDetailIncludesStackTrace(true); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('trace', $payload); + $this->assertIsArray($payload['trace']); + $this->assertEquals($exception->getTrace(), $payload['trace']); + } + + public function testExceptionsCanTriggerInclusionOfNestedExceptions(): void + { + $exceptionChild = new \Exception('child exception'); + $exceptionParent = new \Exception('parent exception', 0, $exceptionChild); + + $apiProblem = new ApiProblem('500', $exceptionParent); + $apiProblem->setDetailIncludesStackTrace(true); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('exception_stack', $payload); + $this->assertIsArray($payload['exception_stack']); + $expected = [ + [ + 'code' => $exceptionChild->getCode(), + 'message' => $exceptionChild->getMessage(), + 'trace' => $exceptionChild->getTrace(), + ], + ]; + $this->assertEquals($expected, $payload['exception_stack']); + } + + public function testTypeUrlIsUsedVerbatim(): void + { + $apiProblem = new ApiProblem('500', 'foo', 'http://status.dev:8080/details.md'); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('type', $payload); + $this->assertEquals('http://status.dev:8080/details.md', $payload['type']); + } + + /** @psalm-return array */ + public static function knownStatusCodesProvider(): array + { + return [ + '404' => [404], + '409' => [409], + '422' => [422], + '500' => [500], + ]; + } + + #[DataProvider('knownStatusCodesProvider')] + public function testKnownStatusResultsInKnownTitle(int $status): void + { + $apiProblem = new ApiProblem($status, 'foo'); + $r = new ReflectionObject($apiProblem); + $p = $r->getProperty('problemStatusTitles'); +// $p->setAccessible(true); + /** @var string[] $titles */ + $titles = $p->getValue($apiProblem); + + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('title', $payload); + $this->assertEquals($titles[$status], $payload['title']); + } + + public function testUnknownStatusResultsInUnknownTitle(): void + { + $apiProblem = new ApiProblem(420, 'foo'); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('title', $payload); + $this->assertEquals('Unknown', $payload['title']); + } + + public function testProvidedTitleIsUsedVerbatim(): void + { + $apiProblem = new ApiProblem('500', 'foo', 'http://status.dev:8080/details.md', 'some title'); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('title', $payload); + $this->assertEquals('some title', $payload['title']); + } + + public function testCanPassArbitraryDetailsToConstructor(): void + { + $problem = new ApiProblem( + 400, + 'Invalid input', + 'http://example.com/api/problem/400', + 'Invalid entity', + ['foo' => 'bar'] + ); + /** @psalm-suppress UndefinedMagicPropertyFetch */ + $this->assertEquals('bar', $problem->foo); + } + + public function testArraySerializationIncludesArbitraryDetails(): void + { + $problem = new ApiProblem( + 400, + 'Invalid input', + 'http://example.com/api/problem/400', + 'Invalid entity', + ['foo' => 'bar'] + ); + $array = $problem->toArray(); + $this->assertArrayHasKey('foo', $array); + $this->assertEquals('bar', $array['foo']); + } + + public function testArbitraryDetailsShouldNotOverwriteRequiredFieldsInArraySerialization(): void + { + $problem = new ApiProblem( + 400, + 'Invalid input', + 'http://example.com/api/problem/400', + 'Invalid entity', + ['title' => 'SHOULD NOT GET THIS'] + ); + $array = $problem->toArray(); + $this->assertArrayHasKey('title', $array); + $this->assertEquals('Invalid entity', $array['title']); + } + + public function testUsesTitleFromExceptionWhenProvided(): void + { + $exception = new Exception\DomainException('exception message', 401); + $exception->setTitle('problem title'); + $apiProblem = new ApiProblem('401', $exception); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('title', $payload); + $this->assertEquals($exception->getTitle(), $payload['title']); + } + + public function testUsesTypeFromExceptionWhenProvided(): void + { + $exception = new Exception\DomainException('exception message', 401); + $exception->setType('http://example.com/api/help/401'); + $apiProblem = new ApiProblem('401', $exception); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('type', $payload); + $this->assertEquals($exception->getType(), $payload['type']); + } + + public function testUsesAdditionalDetailsFromExceptionWhenProvided(): void + { + $exception = new Exception\DomainException('exception message', 401); + $exception->setAdditionalDetails(['foo' => 'bar']); + $apiProblem = new ApiProblem('401', $exception); + $payload = $apiProblem->toArray(); + $this->assertArrayHasKey('foo', $payload); + $this->assertEquals('bar', $payload['foo']); + } + + /** @psalm-return array */ + public static function invalidStatusCodesProvider(): array + { + return [ + '-1' => [-1], + '0' => [0], + '7' => [7], // reported + '14' => [14], // observed + '600' => [600], + ]; + } + + #[DataProvider('invalidStatusCodesProvider')] + public function testInvalidHttpStatusCodesAreCastTo500(int $code): void + { + $e = new \Exception('Testing', $code); + $problem = new ApiProblem($code, $e); + /** @psalm-suppress InaccessibleProperty */ + $this->assertEquals(500, $problem->status); + } } diff --git a/test/Exception/DomainExceptionTest.php b/test/Exception/DomainExceptionTest.php new file mode 100644 index 0000000..7146b17 --- /dev/null +++ b/test/Exception/DomainExceptionTest.php @@ -0,0 +1,25 @@ +assertEquals([], $exception->getAdditionalDetails()); + $this->assertEquals(null, $exception->getType()); + $this->assertEquals(null, $exception->getTitle()); + $exception->setTitle('problem title') + ->setType('exception type') + ->setAdditionalDetails(['foo' => 'bar']); + $this->assertEquals(['foo' => 'bar'], $exception->getAdditionalDetails()); + $this->assertEquals('exception type', $exception->getType()); + $this->assertEquals('problem title', $exception->getTitle()); + } +}