From c1a49be6e1e716d335c9adffb3e12d6907471fd0 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 7 Mar 2024 15:13:35 +0000 Subject: [PATCH] test: pass existing tests for #70 functionality --- phpcs.xml | 1 - src/Chain/ChainFunctionTypeError.php | 6 + src/Chain/Chainable.php | 104 ++++++++++-- src/Chain/ThenChain.php | 3 +- src/Promise.php | 39 ++++- test/phpunit/PromiseTest.php | 221 +++++++++++++++++--------- test/phpunit/TestPromiseContainer.php | 1 - 7 files changed, 280 insertions(+), 95 deletions(-) create mode 100644 src/Chain/ChainFunctionTypeError.php diff --git a/phpcs.xml b/phpcs.xml index b771656..a80be70 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -19,7 +19,6 @@ - diff --git a/src/Chain/ChainFunctionTypeError.php b/src/Chain/ChainFunctionTypeError.php new file mode 100644 index 0000000..cae2369 --- /dev/null +++ b/src/Chain/ChainFunctionTypeError.php @@ -0,0 +1,6 @@ +onRejected, $reason); -// try { -// } -// catch(TypeError $error) { -// $reflection = new ReflectionFunction($this->onRejected); -// $param = $reflection->getParameters()[0] ?? null; -// if($param) { -// $paramType = (string)$param->getType(); -// -// if(!str_contains($error->getMessage(), "must be of type $paramType")) { -// throw $error; -// } -// } -// -// return $reason; -// } + } + + public function checkResolutionCallbackType(mixed $resolvedValue):void { + if(isset($this->onResolved)) { + $this->checkType($resolvedValue, $this->onResolved); + } + } + + public function checkRejectionCallbackType(Throwable $rejection):void { + if(isset($this->onRejected)) { + $this->checkType($rejection, $this->onRejected); + } + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + // phpcs:ignore + private function checkType(mixed $value, callable $callable):void { + if(!$callable instanceof Closure) { + return; + } + + $refFunction = new ReflectionFunction($callable); + $refParameterList = $refFunction->getParameters(); + if(!isset($refParameterList[0])) { + return; + } + $refParameter = $refParameterList[0]; + $nullable = $refParameter->allowsNull(); + + if(is_null($value)) { + if(!$nullable) { + throw new ChainFunctionTypeError("Then function's parameter is not nullable"); + } + } + + $allowedTypes = []; + $refType = $refParameter->getType(); + + if($refType instanceof ReflectionUnionType || $refType instanceof ReflectionIntersectionType) { + foreach($refType->getTypes() as $refSubType) { + array_push($allowedTypes, $refSubType->getName()); + } + } + else { + /** @var ?ReflectionNamedType $refType */ + array_push($allowedTypes, $refType?->getName()); + } + + $valueType = is_object($value) + ? get_class($value) + : gettype($value); + foreach($allowedTypes as $allowedType) { + $allowedType = match($allowedType) { + "int" => "integer", + "float" => "double", + default => $allowedType, + }; + if(is_null($allowedType) || $allowedType === "mixed") { +// A typeless property is defined - allow anything! + return; + } + if($allowedType === $valueType) { + return; + } + + if(is_a($valueType, $allowedType, true)) { + return; + } + + if($allowedType === "string") { + if($valueType === "double" || $valueType === "integer") { + return; + } + } + if($allowedType === "double") { + if(is_numeric($value)) { + return; + } + } + } + + throw new ChainFunctionTypeError("Value $value is not compatible with chainable parameter"); } } diff --git a/src/Chain/ThenChain.php b/src/Chain/ThenChain.php index 6953868..7ea761a 100644 --- a/src/Chain/ThenChain.php +++ b/src/Chain/ThenChain.php @@ -1,4 +1,5 @@ rejectedReason)) { return PromiseState::REJECTED; } - elseif(isset($this->resolvedValue)) { + elseif($this->resolvedValueSet) { return PromiseState::RESOLVED; } @@ -87,7 +90,12 @@ private function callExecutor():void { call_user_func( $this->executor, function(mixed $value = null) { - $this->resolve($value); + try { + $this->resolve($value); + } + catch(PromiseException $exception) { + $this->reject($exception); + } }, function(Throwable $reason) { $this->reject($reason); @@ -107,6 +115,7 @@ private function resolve(mixed $value):void { } $this->resolvedValue = $value; + $this->resolvedValueSet = true; } private function reject(Throwable $reason):void { @@ -133,6 +142,8 @@ private function tryComplete():void { } } + /** @SuppressWarnings(PHPMD.CyclomaticComplexity) */ + // phpcs:ignore private function complete():void { usort( $this->chain, @@ -155,11 +166,28 @@ function(Chainable $a, Chainable $b) { } if($chainItem instanceof ThenChain) { + try { + if($this->resolvedValueSet) { + $chainItem->checkResolutionCallbackType($this->resolvedValue); + } + } + catch(ChainFunctionTypeError) { + continue; + } + $this->handleThen($chainItem); } elseif($chainItem instanceof CatchChain) { - if($handled = $this->handleCatch($chainItem)) { - array_push($this->handledRejections, $handled); + try { + if(isset($this->rejectedReason)) { + $chainItem->checkRejectionCallbackType($this->rejectedReason); + } + if($handled = $this->handleCatch($chainItem)) { + array_push($this->handledRejections, $handled); + } + } + catch(ChainFunctionTypeError) { + continue; } } elseif($chainItem instanceof FinallyChain) { @@ -180,8 +208,7 @@ private function handleThen(ThenChain $then):void { } try { - $result = $then->callOnResolved($this->resolvedValue) - ?? $this->resolvedValue ?? null; + $result = $then->callOnResolved($this->resolvedValue); if($result instanceof PromiseInterface) { $this->chainPromise($result); diff --git a/test/phpunit/PromiseTest.php b/test/phpunit/PromiseTest.php index eea2a81..88ccfa1 100644 --- a/test/phpunit/PromiseTest.php +++ b/test/phpunit/PromiseTest.php @@ -1,6 +1,7 @@ getTestPromiseContainer(); $sut = $promiseContainer->getPromise(); - $sut->then(function() {})->then( + $sut->then(function(string $message) { + return $message; + })->then( self::mockCallable(1, $value) ); @@ -48,12 +52,12 @@ public function testPromiseRejectsIfResolvedWithItself() { $promiseContainer = $this->getTestPromiseContainer(); $sut = $promiseContainer->getPromise(); $onResolvedCallCount = 0; - $sut->then(function($value) use(&$onResolvedCallCount) { + $sut->then(function($value) use (&$onResolvedCallCount) { $onResolvedCallCount++; }) - ->catch(function(PromiseException $reason) use(&$actualMessage) { - $actualMessage = $reason->getMessage(); - }); + ->catch(function(PromiseException $reason) use (&$actualMessage) { + $actualMessage = $reason->getMessage(); + }); $promiseContainer->resolve($sut); self::assertEquals(0, $onResolvedCallCount); @@ -67,9 +71,9 @@ public function testRejectWithException() { $fulfilledCallCount = 0; - $sut->then(function() use(&$fulfilledCallCount) { - $fulfilledCallCount++; - }) + $sut->then(function() use (&$fulfilledCallCount) { + $fulfilledCallCount++; + }) ->catch(self::mockCallable(1, $exception)); $promiseContainer->reject($exception); @@ -92,7 +96,7 @@ public function testRejectIfFulfillerThrowsException() { $sut = $promiseContainer->getPromise(); $sut->then( - function() use($exception) { + function() use ($exception) { throw $exception; }, self::mockCallable(0), @@ -111,11 +115,11 @@ public function testRejectIfRejecterThrowsException() { $sut = $promiseContainer->getPromise(); $sut->then(self::mockCallable(0)) - ->catch(function(Throwable $reason) use($exception) { + ->catch(function(Throwable $reason) use ($exception) { throw $exception; }) ->then(self::mockCallable(0)) - ->catch(function(Throwable $reason) use(&$caughtExceptions) { + ->catch(function(Throwable $reason) use (&$caughtExceptions) { array_push($caughtExceptions, $reason); }); @@ -156,14 +160,25 @@ public function testThenResultForwardedWhenOnFulfilledIsNull() { $promiseContainer = $this->getTestPromiseContainer(); - $onFulfilled = self::mockCallable(2, $message); - $onRejected = self::mockCallable(0); + $caughtResolutions = []; + $caughtReasons = []; $sut = $promiseContainer->getPromise(); - $sut->then($onFulfilled)->catch($onRejected) - ->then($onFulfilled)->catch($onRejected); + $sut->then(function(string $resolved) use (&$caughtResolutions) { + array_push($caughtResolutions, $resolved); + return $resolved; + })->catch(function(Throwable $reason) use (&$caughtReasons) { + array_push($caughtReasons, $reason); + })->then(function(string $resolved) use (&$caughtResolutions) { + array_push($caughtResolutions, $resolved); + return $resolved; + })->catch(function(Throwable $reason) use (&$caughtReasons) { + array_push($caughtReasons, $reason); + }); $promiseContainer->resolve($message); + self::assertCount(2, $caughtResolutions); + self::assertEmpty($caughtReasons); } public function testThenCallbackResultForwarded() { @@ -180,18 +195,56 @@ public function testThenCallbackResultForwarded() { ); $onRejected = self::mockCallable(0); - $sut->then(function(string $message) use($messageConcat) { + $sut->then(function(string $message) use ($messageConcat) { return "$message, $messageConcat"; }) ->then(function(string $message) { - return "$message!!!"; - }) + return "$message!!!"; + }) ->then($onFulfilled) ->catch($onRejected); $promiseContainer->resolve($message); } + public function testThenCallbackResultStoppedWhenNullReturn() { + $concatMessages1 = ""; + $promiseContainer = $this->getTestPromiseContainer(); + $sut = $promiseContainer->getPromise(); + $sut->then(function(string $message) use (&$concatMessages1) { + $concatMessages1 .= $message; + + if($message === "STOP") { + return null; + } + + return "MORE"; + })->then(function(string $message) use (&$concatMessages1) { + $concatMessages1 .= $message; + }); + + $promiseContainer->resolve("HELLO"); + self::assertEquals("HELLOMORE", $concatMessages1); + + $concatMessages2 = ""; + $promiseContainer = $this->getTestPromiseContainer(); + $sut = $promiseContainer->getPromise(); + $sut->then(function(string $message) use (&$concatMessages2) { + $concatMessages2 .= $message; + + if($message === "STOP") { + return null; + } + + return "MORE"; + })->then(function(string $message) use (&$concatMessages2) { + $concatMessages2 .= $message; + }); + + $promiseContainer->resolve("STOP"); + self::assertEquals("STOP", $concatMessages2); + } + /** * A rejected promise should forward its rejection to the end of the * promise chain. @@ -204,16 +257,16 @@ public function testThenRejectionCallbackResultForwarded() { $fulfilledCallCount = 0; $sut = $promiseContainer->getPromise(); - $sut->then(function($value) use(&$fulfilledCallCount) { - $fulfilledCallCount++; - }) - ->then(function($value) use(&$fulfilledCallCount) { + $sut->then(function($value) use (&$fulfilledCallCount) { $fulfilledCallCount++; }) - ->then(function($value) use(&$fulfilledCallCount) { + ->then(function($value) use (&$fulfilledCallCount) { + $fulfilledCallCount++; + }) + ->then(function($value) use (&$fulfilledCallCount) { $fulfilledCallCount++; }) - ->catch(self::mockCallable(1, $expectedException)); + ->catch(self::mockCallable(1, $expectedException)); $promiseContainer->reject($expectedException); self::assertEquals(0, $fulfilledCallCount); @@ -259,7 +312,7 @@ public function testCatchRejectionReasonIdenticalToRejectionException() { $onRejected = self::mockCallable(1, $exception); $sut = $promiseContainer->getPromise(); - $sut->catch(function($reason) use($onRejected) { + $sut->catch(function($reason) use ($onRejected) { call_user_func($onRejected, $reason); }); $promiseContainer->reject($exception); @@ -272,7 +325,7 @@ public function testCatchRejectionHandlerIsCalledByTypeHintedOnRejectedCallback( $onRejected = self::mockCallable(1, $exception); - $sut->catch(function(PromiseException $reason) use($onRejected) { + $sut->catch(function(PromiseException $reason) use ($onRejected) { call_user_func($onRejected, $reason); }); @@ -288,10 +341,10 @@ public function testCatchRejectionWhenExceptionIsThrownInResolutionFunction() { $expectedReason = new RuntimeException("This is expected"); $caughtReasons = []; - $sut->then(function($value)use ($expectedReason, &$caughtResolutions) { + $sut->then(function($value) use ($expectedReason, &$caughtResolutions) { array_push($caughtResolutions, $value); throw $expectedReason; - })->catch(function(Throwable $reason)use(&$caughtReasons) { + })->catch(function(Throwable $reason) use (&$caughtReasons) { array_push($caughtReasons, $reason); }); @@ -315,15 +368,15 @@ public function testCatchRejectionWhenExceptionIsThrownInResolutionFunctionUsing $expectedReason = new RuntimeException("This is expected"); $caughtReasons = []; - $sut->then(function($value)use ($expectedReason) { + $sut->then(function($value) use ($expectedReason) { throw $expectedReason; - })->catch(function(Throwable $reason)use($newDeferred) { + })->catch(function(Throwable $reason) use ($newDeferred) { $newDeferred->reject($reason); }); - $newPromise->then(function($value)use ($expectedReason, &$caughtResolutions) { + $newPromise->then(function($value) use ($expectedReason, &$caughtResolutions) { array_push($caughtResolutions, $value); - })->catch(function(Throwable $reason)use (&$caughtReasons) { + })->catch(function(Throwable $reason) use (&$caughtReasons) { array_push($caughtReasons, $reason); }); @@ -347,7 +400,7 @@ public function testFinallyDoesNotBlockOnFulfilled() { $sut = $promiseContainer->getPromise(); $sut->finally(fn() => "example123") - ->then(self::mockCallable(1, $expectedValue)); + ->then(self::mockCallable(1, $expectedValue)); $promiseContainer->resolve($expectedValue); } @@ -357,7 +410,7 @@ public function testFinallyDoesNotBlockOnRejected() { $promiseContainer = $this->getTestPromiseContainer(); $sut = $promiseContainer->getPromise(); $sut->finally(function() {}) - ->catch(self::mockCallable(1, $exception)); + ->catch(self::mockCallable(1, $exception)); $promiseContainer->reject($exception); } @@ -380,12 +433,12 @@ public function testFinallyPassesThrownException() { self::expectException(Exception::class); self::expectExceptionMessage("Second"); $sut = $promiseContainer->getPromise(); - $sut->finally(function(mixed $resolvedValueOrRejectedReason) use($exception1) { + $sut->finally(function(mixed $resolvedValueOrRejectedReason) use ($exception1) { self::assertSame($resolvedValueOrRejectedReason, $exception1); throw new Exception("Second"); }) - ->then(self::mockCallable(0)) - ->catch(self::mockCallable(1, $exception1)); + ->then(self::mockCallable(0)) + ->catch(self::mockCallable(1, $exception1)); $promiseContainer->reject($exception1); } @@ -396,7 +449,7 @@ public function testOnRejectedCalledWhenFinallyThrows() { self::expectException(PromiseException::class); self::expectExceptionMessage("Oh dear, oh dear"); $sut = $promiseContainer->getPromise(); - $sut->finally(function() use($exception) { + $sut->finally(function() use ($exception) { throw $exception; })->then( self::mockCallable(1, "Example resolution"), @@ -428,7 +481,7 @@ public function testGetStateFulfilled() { public function testGetStateRejected() { $promiseContainer = $this->getTestPromiseContainer(); $sut = $promiseContainer->getPromise(); - $sut->catch(function(Throwable $throwable){}); + $sut->catch(function(Throwable $throwable) {}); $promiseContainer->reject(new Exception("Example rejection")); @@ -452,12 +505,12 @@ public function testCatchMethodNotBubblesThrowables() { $exception = null; try { - $sut->then(function() use($expectedException) { + $sut->then(function() use ($expectedException) { throw $expectedException; }) - ->catch($onRejected); + ->catch($onRejected); + } catch(Throwable $exception) { } - catch(Throwable $exception) {} $promiseContainer->resolve("test"); self::assertNull($exception); @@ -472,15 +525,15 @@ public function testNoCatchMethodBubblesThrowables() { $expectedException = new Exception("Test exception"); $promiseContainer = $this->getTestPromiseContainer(); $sut = $promiseContainer->getPromise(); - $sut->then(function() use($expectedException) { + $sut->then(function() use ($expectedException) { throw $expectedException; }); $exception = null; try { $promiseContainer->resolve("test"); + } catch(Throwable $exception) { } - catch(Throwable $exception) {} self::assertSame($expectedException, $exception); } @@ -492,16 +545,16 @@ public function testNoCatchMethodBubblesThrowables_internalRejection() { $exception = null; try { - $sut->then(function(string $message) use($sut, $promiseContainer, $expectedException) { - $sut->then(function($resolvedValue) use($promiseContainer, $expectedException) { + $sut->then(function(string $message) use ($sut, $promiseContainer, $expectedException) { + $sut->then(function($resolvedValue) use ($promiseContainer, $expectedException) { $promiseContainer->reject($expectedException); }); return $sut; }); $promiseContainer->resolve("test"); + } catch(Throwable $exception) { } - catch(Throwable $exception) {} self::assertSame($expectedException, $exception); } @@ -514,18 +567,24 @@ public function testFulfilledReturnsNewPromiseThatIsResolved() { $messagePromise = $messagePromiseContainer->getPromise(); $numberToResolveWith = null; + $actualMessageReceived = null; // The first onFulfilled takes the number to process, and returns a new promise // which should resolve to a message containing the number. $numberPromise - ->then(function(int $number) use($messagePromiseContainer, $messagePromise, &$numberToResolveWith) { - $numberToResolveWith = $number; - return $messagePromise; - }) - ->then(self::mockCallable(1, "Your number is 105")); + ->then(function(int $number) use ($messagePromiseContainer, $messagePromise, &$numberToResolveWith) { + $numberToResolveWith = $number; + return $messagePromise; + }) + ->then( + function(string $message) use(&$actualMessageReceived) { + $actualMessageReceived = $message; + } + ); $numberPromiseContainer->resolve(105); $messagePromiseContainer->resolve("Your number is $numberToResolveWith"); + self::assertEquals("Your number is 105", $actualMessageReceived); } /** @@ -558,24 +617,24 @@ public function testFulfilledReturnsNewPromiseThatIsResolved2() { $innerComplete = null; $innerPromise = null; - $sut = new Promise(function($f, $r, $c) use(&$fulfill, &$reject, &$complete) { + $sut = new Promise(function($f, $r, $c) use (&$fulfill, &$reject, &$complete) { $fulfill = $f; $reject = $r; $complete = $c; }); // Define asynchronous behaviour: - $sut->then(function(string $name) use(&$innerFulfil, &$innerReject, &$innerComplete, &$innerPromise, &$searchTerm, &$receivedNames) { + $sut->then(function(string $name) use (&$innerFulfil, &$innerReject, &$innerComplete, &$innerPromise, &$searchTerm, &$receivedNames) { array_push($receivedNames, $name); $searchTerm = $name; - $innerPromise = new Promise(function($f, $r, $c) use(&$innerFulfil, &$innerReject, &$innerComplete) { + $innerPromise = new Promise(function($f, $r, $c) use (&$innerFulfil, &$innerReject, &$innerComplete) { $innerFulfil = $f; $innerReject = $r; $innerComplete = $c; }); return $innerPromise; - })->then(function(string $address) use(&$receivedAddresses) { + })->then(function(string $address) use (&$receivedAddresses) { array_push($receivedAddresses, $address); }); @@ -607,18 +666,18 @@ public function testCustomPromise_resolve() { $newPromise = new CustomPromise(); $deferred = new Deferred(); $deferredPromise = $deferred->getPromise(); - $deferredPromise->then(function($resolvedValue)use($newPromise) { + $deferredPromise->then(function($resolvedValue) use ($newPromise) { $newPromise->resolve($resolvedValue); - }, function($rejectedValue)use($newPromise) { + }, function($rejectedValue) use ($newPromise) { $newPromise->reject($rejectedValue); }); $resolution = null; $rejection = null; - $newPromise->then(function($resolvedValue)use(&$resolution) { + $newPromise->then(function($resolvedValue) use (&$resolution) { $resolution = $resolvedValue; - }, function($rejectedValue)use(&$rejection) { + }, function($rejectedValue) use (&$rejection) { $rejection = $rejectedValue; }); @@ -635,18 +694,18 @@ public function testCustomPromise_reject() { $deferred = new Deferred(); $deferredPromise = $deferred->getPromise(); - $deferredPromise->then(function($resolvedValue)use($customPromise) { + $deferredPromise->then(function($resolvedValue) use ($customPromise) { $customPromise->resolve($resolvedValue); - })->catch(function($rejectedValue)use($customPromise) { + })->catch(function($rejectedValue) use ($customPromise) { $customPromise->reject($rejectedValue); }); $resolution = null; $rejection = null; - $customPromise->then(function($resolvedValue)use(&$resolution) { + $customPromise->then(function($resolvedValue) use (&$resolution) { $resolution = $resolvedValue; - })->catch(function($rejectedValue)use(&$rejection) { + })->catch(function($rejectedValue) use (&$rejection) { $rejection = $rejectedValue; }); @@ -665,9 +724,9 @@ public function testPromise_rejectChain() { $deferred = new Deferred(); $deferredPromise = $deferred->getPromise(); - $deferredPromise->then(function($resolvedValue)use(&$thenCalls) { + $deferredPromise->then(function($resolvedValue) use (&$thenCalls) { array_push($thenCalls, $resolvedValue); - })->catch(function(Throwable $reason)use(&$catchCalls) { + })->catch(function(Throwable $reason) use (&$catchCalls) { array_push($catchCalls, $reason); }); @@ -675,11 +734,11 @@ public function testPromise_rejectChain() { $innerPromise = $innerDeferred->getPromise(); $rejection = new Exception("test rejection"); - $innerPromise->then(function(string $message)use($rejection) { + $innerPromise->then(function(string $message) use ($rejection) { if(!$message) { throw $rejection; } - })->catch(function(Throwable $reason)use($deferred) { + })->catch(function(Throwable $reason) use ($deferred) { $deferred->reject($reason); }); @@ -699,7 +758,7 @@ public function testPromise_notThrowWhenNoCatch():void { if($message === "error") { throw $expectedException; } - })->catch(function(Throwable $reason) use(&$caughtReasons) { + })->catch(function(Throwable $reason) use (&$caughtReasons) { array_push($caughtReasons, $reason); }); @@ -713,7 +772,7 @@ public function testPromise_throwWhenNoCatch():void { $deferred = new Deferred(); $deferredPromise = $deferred->getPromise(); - $deferredPromise->then(function(string $message) use($expectedException) { + $deferredPromise->then(function(string $message) use ($expectedException) { if($message === "error") { throw $expectedException; } @@ -724,13 +783,33 @@ public function testPromise_throwWhenNoCatch():void { $deferred->resolve("error"); } + public function testPromise_catchMethodCorrectType():void { + $deferred = new Deferred(); + $promise = $deferred->getPromise(); + $resolvedValue = null; + $rejectedReason = null; + + $promise->then(function(string $value) use(&$resolvedValue) { + $resolvedValue = $value; + })->catch(function(ValueError $valueError) use(&$rejectedReason) { + $rejectedReason = $valueError; + })->catch(function(ArithmeticError $arithmeticError) use(&$rejectedReason) { + $rejectedReason = $arithmeticError; + }); + + $triggeredError = new ArithmeticError("something bad happened"); + $deferred->reject($triggeredError); + self::assertNull($resolvedValue); + self::assertSame($triggeredError, $rejectedReason); + } + protected function getTestPromiseContainer():TestPromiseContainer { $resolveCallback = null; $rejectCallback = null; $completeCallback = null; $promise = new Promise(function($resolve, $reject, $complete) - use(&$resolveCallback, &$rejectCallback, &$completeCallback) { + use (&$resolveCallback, &$rejectCallback, &$completeCallback) { $resolveCallback = $resolve; $rejectCallback = $reject; $completeCallback = $complete; diff --git a/test/phpunit/TestPromiseContainer.php b/test/phpunit/TestPromiseContainer.php index 6d31d23..13b00a7 100644 --- a/test/phpunit/TestPromiseContainer.php +++ b/test/phpunit/TestPromiseContainer.php @@ -3,7 +3,6 @@ use Gt\Promise\PromiseInterface; use Gt\Promise\PromiseState; -use Http\Promise\Promise as HttpPromiseInterface; class TestPromiseContainer { private PromiseInterface $promise;