From 2360ab237cf39f5ae18cf0e69fb7952a93370db0 Mon Sep 17 00:00:00 2001 From: Andrej Rypo Date: Mon, 16 Oct 2023 12:16:05 +0200 Subject: [PATCH] Test everything --- tests/bootstrap.php | 16 +++ tests/custom.phpt | 133 +++++++++++++++++++++ tests/errorContainer.phpt | 53 +++++++++ tests/http.phpt | 137 ++++++++++++++++++++++ tests/overriding.phpt | 65 +++++++++++ tests/serverside.phpt | 36 ++++++ tests/tests.php | 236 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 676 insertions(+) create mode 100644 tests/bootstrap.php create mode 100644 tests/custom.phpt create mode 100644 tests/errorContainer.phpt create mode 100644 tests/http.phpt create mode 100644 tests/overriding.phpt create mode 100644 tests/serverside.phpt create mode 100644 tests/tests.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..2207cc4 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,16 @@ +getMessage(); + } +} + +testCustomException( + instance: new ServerFault(), + interfaces: [IndicatesServerFault::class], + httpStatusCode: 500, + httpStatusMessage: 'Internal server error', +); + +testCustomException( + instance: new DefaultClientFault(), + interfaces: [IndicatesClientFault::class], + httpStatusCode: 400, + httpStatusMessage: 'Bad request', +); + +testCustomException( + instance: new ThirdPartyFault(), + interfaces: [IndicatesThirdPartyFault::class], + httpStatusCode: 500, + httpStatusMessage: 'Internal server error', +); + + +testPublicConveying( + new ServerFault('Whatever happens, we do not want the clients see this message.'), + fn(): string => 'A system error has occurred. We are sorry for the inconvenience.', +); +testPublicConveying( + new ThirdPartyFault('Whatever happens, we do not want the clients see this message.'), + fn(): string => 'A system error has occurred. We are sorry for the inconvenience.', +); + +// Note: The only way to make `convey` method expose the internal exception message is via overriding the `getDefaultMessageToConvey` method by the exception. +testPublicConveying( + new DefaultClientFault('Whatever happens, we do not want the clients see this message.'), + fn(): string => 'A system error has occurred. We are sorry for the inconvenience.', +); + +// Note: Conveying the internal message by overriding the `getDefaultMessageToConvey` method. +testPublicConveying( + new GenericClientFault('Yeah, this should be the message for the client.'), + fn(): string => 'Yeah, this should be the message for the client.', +); diff --git a/tests/errorContainer.phpt b/tests/errorContainer.phpt new file mode 100644 index 0000000..b008675 --- /dev/null +++ b/tests/errorContainer.phpt @@ -0,0 +1,53 @@ + 'metadata'], + status: 400, + code: 'crashed...somehow', +); + +Assert::same('An important message.', $ec->message); +Assert::same('input.email', $ec->source); +Assert::same('A description for humans.', $ec->detail); +Assert::same(['any' => 'metadata'], $ec->meta); +Assert::same(400, $ec->status); +Assert::same('crashed...somehow', $ec->code); + +Assert::same([ + 'message' => 'An important message.', + 'detail' => 'A description for humans.', + 'source' => 'input.email', + 'code' => 'crashed...somehow', + 'status' => 400, + 'meta' => ['any' => 'metadata'], +], $ec->jsonSerialize()); + +// test that all public props are present in json +$rf = new ReflectionClass(ErrorContainer::class); +Assert::same(count($rf->getProperties()), count($ec->jsonSerialize())); + +// test that null values are omitted when serializing +$ec2 = new ErrorContainer('only the message is set'); +Assert::same(1, count($ec2->jsonSerialize())); +$ec3 = new ErrorContainer(meta: null); +Assert::same([], $ec3->jsonSerialize()); + +// but string (even empty) are not +$ec4 = new ErrorContainer(message: '', detail: '', code: null); +Assert::same(2, count($ec4->jsonSerialize())); diff --git a/tests/http.phpt b/tests/http.phpt new file mode 100644 index 0000000..1fe1279 --- /dev/null +++ b/tests/http.phpt @@ -0,0 +1,137 @@ + $e->getMessage(), + ); + + // http status/message + testHttpStatusCodeSuggestion($instance, $httpStatusCode); + testErrorMessageSuggestion($instance, $httpStatusMessage); +} + +// Here we test the HTTP exceptions: +testHttpException( + instance: new BadRequest(), + interfaces: [], + httpStatusCode: 400, + httpStatusMessage: 'Bad request', +); + +testHttpException( + instance: new Unauthorized(), + interfaces: [IndicatesAuthenticationFault::class], + httpStatusCode: 401, + httpStatusMessage: 'Unauthorized', +); + +testHttpException( + instance: new Forbidden(), + interfaces: [IndicatesAuthorizationFault::class], + httpStatusCode: 403, + httpStatusMessage: 'Forbidden', +); + +testHttpException( + instance: new NotFound(), + interfaces: [IndicatesMissingResource::class], + httpStatusCode: 404, + httpStatusMessage: 'Not found', +); + +testHttpException( + instance: new Conflict(), + interfaces: [IndicatesConflict::class], + httpStatusCode: 409, + httpStatusMessage: 'Conflict', +); + +testHttpException( + instance: new UnprocessableContent(), + interfaces: [IndicatesInvalidInput::class], + httpStatusCode: 422, + httpStatusMessage: 'Unprocessable content', +); + +testHttpException( + instance: new ImATeapot(), + interfaces: [IndicatesGoodMood::class], + httpStatusCode: 418, + httpStatusMessage: 'I\'m a teapot', +); + + +testHttpException( + instance: new GenericClientHttpException(), + interfaces: [], + httpStatusCode: 400, + httpStatusMessage: 'Bad request', +); +testHttpException( + instance: new GenericClientHttpException('Oops!', 499), + interfaces: [], + httpStatusCode: 400, + httpStatusMessage: 'Bad request', + defaultConveyMessageGetter: fn() => 'Oops!', +); +testHttpException( + instance: (new GenericClientHttpException('Oops!'))->setHttpStatus(499), + interfaces: [], + httpStatusCode: 499, + httpStatusMessage: 'Bad request', + defaultConveyMessageGetter: fn() => 'Oops!', +); + diff --git a/tests/overriding.phpt b/tests/overriding.phpt new file mode 100644 index 0000000..5b48f75 --- /dev/null +++ b/tests/overriding.phpt @@ -0,0 +1,65 @@ + $instance->getDefaultMessageToConvey()); +} + +// Here we test the 2 server-side exceptions: +testServerSideException(LogicException::class); +testServerSideException(RuntimeException::class); diff --git a/tests/tests.php b/tests/tests.php new file mode 100644 index 0000000..8935ed7 --- /dev/null +++ b/tests/tests.php @@ -0,0 +1,236 @@ +getMessage()); + Assert::same(123456783, $instance->getCode()); + Assert::same($previous, $instance->getPrevious()); +} + +function testExplanation(Throwable $e): void +{ + Assert::type(SupportsInternalExplanation::class, $e); + + // No default explanation. + Assert::same(null, $e->explanation()); + + // Set/Get. + $e->explain('Well explained indeed.'); + Assert::same('Well explained indeed.', $e->explanation()); + + // Nope. + Assert::throws(fn() => $e->explain(1234), TypeError::class); + Assert::throws(fn() => $e->explain([]), TypeError::class); + Assert::throws(fn() => $e->explain(new stdClass()), TypeError::class); + + // Reset. + $e->explain(null); + Assert::same(null, $e->explanation()); +} + +/** + * @param SupportsInternalContext&Throwable $e + */ +function testInternalContext(Throwable $e): void +{ + _testContext($e, SupportsInternalContext::class, 'pin', 'context', 'replaceContext'); +} + +/** + * @param SupportsPublicContext&Throwable $e + */ +function testPublicContext(Throwable $e): void +{ + _testContext($e, SupportsPublicContext::class, 'pass', 'publicContext', 'replacePublicContext'); +} + +function _testContext( + Throwable $e, + string $interface, + string $insertMethod, + string $retrieveMethod, + string $overwriteMethod +): void { + Assert::type($interface, $e); + + Assert::same([], $e->{$retrieveMethod}()); + + $e->{$insertMethod}('some value'); + Assert::same(['some value'], $e->{$retrieveMethod}()); + + // pinning a null has no effect + $e->{$insertMethod}(null); + Assert::same(['some value'], $e->{$retrieveMethod}()); + + // pinning a null under a specific key removes the key + $e->{$insertMethod}(null, '0'); + Assert::same([], $e->{$retrieveMethod}()); + + $e->{$insertMethod}('value-1', 'key-1'); + $e->{$insertMethod}('value-2', 'key-2'); + $e->{$insertMethod}('value-3', 'key-3'); + $e->{$insertMethod}('value-42', 42); + Assert::same(['key-1' => 'value-1', 'key-2' => 'value-2', 'key-3' => 'value-3', 42 => 'value-42'], $e->{$retrieveMethod}()); + + // removing a value under the key with null + $e->{$insertMethod}(null, 'key-2'); + Assert::same(['key-1' => 'value-1', 'key-3' => 'value-3', 42 => 'value-42'], $e->{$retrieveMethod}()); + + // reset + $e->{$overwriteMethod}([]); + Assert::same([], $e->{$retrieveMethod}()); + + // It is possible to pin value of any type. + $e->{$insertMethod}(1234); + $e->{$insertMethod}(['foo' => 'bar'], 'an array'); + $e->{$insertMethod}($e, 'self'); + Assert::same([ + 1234, + 'an array' => ['foo' => 'bar'], + 'self' => $e, + ], $e->{$retrieveMethod}()); + + // reset + $e->{$overwriteMethod}([]); + Assert::same([], $e->{$retrieveMethod}()); +} + +/** + * @param SupportsTagging&Throwable $e + */ +function testTagging(Throwable $e): void +{ + Assert::type(SupportsTagging::class, $e); + + Assert::same([], $e->tags()); + + $e->tag('default tag'); + Assert::same(['default tag'], $e->tags()); + $e->tag('another tag'); + Assert::same(['default tag', 'another tag'], $e->tags()); + + // reset + $e->replaceTags([]); + Assert::same([], $e->tags()); + + $e->tag('value-1', 'key-1'); + $e->tag('value-2', 'key-2'); + $e->tag('value-3', 'key-3'); + $e->tag('value-42', 42); + Assert::same(['key-1' => 'value-1', 'key-2' => 'value-2', 'key-3' => 'value-3', 42 => 'value-42'], $e->tags()); + + Assert::throws(fn() => $e->tag(null), TypeError::class); + Assert::throws(fn() => $e->tag(1234), TypeError::class); + Assert::throws(fn() => $e->replaceTags([12345]), TypeError::class); + + // reset + $e->replaceTags([]); + Assert::same([], $e->tags()); +} + +/** + * @param SupportsPublicConveying&SupportsPublicContext&Throwable $e + */ +function testPublicConveying(Throwable $e, callable $defaultConveyingMessageGetter): void +{ + Assert::type(SupportsPublicConveying::class, $e); + Assert::type(SupportsPublicContext::class, $e); + + // first call to convey detailed data + $e->convey( + 'An important message.', + 'input.email', + 'A description for humans.', + ['any' => 'metadata'], + 400, + 'crashed...somehow', + ); + // second call without parameters (should use defaults) + $e->convey(); + + $context = $e->publicContext(); + Assert::same(2, count($context)); + + /** @var ErrorContainer $container */ + $container = $context[0] ?? null; + Assert::type(ErrorContainer::class, $container); + Assert::same('An important message.', $container->message); + Assert::same('input.email', $container->source); + Assert::same('A description for humans.', $container->detail); + Assert::same(['any' => 'metadata'], $container->meta); + Assert::same(400, $container->status); + Assert::same('crashed...somehow', $container->code); + + // here we test that without the parameters to `convey` method, the container receives the expected default message and nothing else + $defaultContainer = $context[1] ?? null; + Assert::type(ErrorContainer::class, $defaultContainer); + Assert::same($defaultConveyingMessageGetter($e), $defaultContainer->message); + Assert::same(null, $defaultContainer->source); + Assert::same(null, $defaultContainer->detail); + Assert::same(null, $defaultContainer->meta); + Assert::same(null, $defaultContainer->status); + Assert::same(null, $defaultContainer->code); +} + +function testDefaultConveyingMessage(Throwable $e, string $expectedMessage):void{ + Assert::type(SupportsPublicConveying::class, $e); + Assert::type(SupportsPublicContext::class, $e); + + + +} + + +/** + * @param SuggestsHttpStatus&Throwable $e + */ +function testHttpStatusCodeSuggestion(Throwable $e, int $code): void +{ + Assert::type(SuggestsHttpStatus::class, $e); + + Assert::same($code, $e->suggestStatusCode()); +} + +/** + * @param SuggestsErrorMessage&Throwable $e + */ +function testErrorMessageSuggestion(Throwable $e, string $message): void +{ + Assert::type(SuggestsErrorMessage::class, $e); + + Assert::same($message, $e->suggestErrorMessage()); +}