httpClient = $this->prophesize(ClientInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); } /** @test */ public function emptyWebhooksMakeNoFurtherActions(): void { $find = $this->em->find(Visit::class, '1')->willReturn(null); $this->createListener([])(new VisitLocated('1')); $find->shouldNotHaveBeenCalled(); } /** @test */ public function invalidVisitDoesNotPerformAnyRequest(): void { $find = $this->em->find(Visit::class, '1')->willReturn(null); $requestAsync = $this->httpClient->requestAsync( RequestMethodInterface::METHOD_POST, Argument::type('string'), Argument::type('array'), )->willReturn(new FulfilledPromise('')); $logWarning = $this->logger->warning( 'Tried to notify webhooks for visit with id "{visitId}", but it does not exist.', ['visitId' => '1'], ); $this->createListener(['foo', 'bar'])(new VisitLocated('1')); $find->shouldHaveBeenCalledOnce(); $logWarning->shouldHaveBeenCalledOnce(); $requestAsync->shouldNotHaveBeenCalled(); } /** @test */ public function orphanVisitDoesNotPerformAnyRequestWhenDisabled(): void { $find = $this->em->find(Visit::class, '1')->willReturn(Visit::forBasePath(Visitor::emptyInstance())); $requestAsync = $this->httpClient->requestAsync( RequestMethodInterface::METHOD_POST, Argument::type('string'), Argument::type('array'), )->willReturn(new FulfilledPromise('')); $logWarning = $this->logger->warning(Argument::cetera()); $this->createListener(['foo', 'bar'], false)(new VisitLocated('1')); $find->shouldHaveBeenCalledOnce(); $logWarning->shouldNotHaveBeenCalled(); $requestAsync->shouldNotHaveBeenCalled(); } /** * @test * @dataProvider provideVisits */ public function expectedRequestsArePerformedToWebhooks(Visit $visit, array $expectedResponseKeys): void { $webhooks = ['foo', 'invalid', 'bar', 'baz']; $invalidWebhooks = ['invalid', 'baz']; $find = $this->em->find(Visit::class, '1')->willReturn($visit); $requestAsync = $this->httpClient->requestAsync( RequestMethodInterface::METHOD_POST, Argument::type('string'), Argument::that(function (array $requestOptions) use ($expectedResponseKeys) { Assert::assertArrayHasKey(RequestOptions::HEADERS, $requestOptions); Assert::assertArrayHasKey(RequestOptions::JSON, $requestOptions); Assert::assertArrayHasKey(RequestOptions::TIMEOUT, $requestOptions); Assert::assertEquals($requestOptions[RequestOptions::TIMEOUT], 10); Assert::assertEquals($requestOptions[RequestOptions::HEADERS], ['User-Agent' => 'Shlink:v1.2.3']); $json = $requestOptions[RequestOptions::JSON]; Assert::assertCount(count($expectedResponseKeys), $json); foreach ($expectedResponseKeys as $key) { Assert::assertArrayHasKey($key, $json); } return $requestOptions; }), )->will(function (array $args) use ($invalidWebhooks) { [, $webhook] = $args; $shouldReject = contains($invalidWebhooks, $webhook); return $shouldReject ? new RejectedPromise(new Exception('')) : new FulfilledPromise(''); }); $logWarning = $this->logger->warning( 'Failed to notify visit with id "{visitId}" to webhook "{webhook}". {e}', Argument::that(function (array $extra) { Assert::assertArrayHasKey('webhook', $extra); Assert::assertArrayHasKey('visitId', $extra); Assert::assertArrayHasKey('e', $extra); return $extra; }), ); $this->createListener($webhooks)(new VisitLocated('1')); $find->shouldHaveBeenCalledOnce(); $requestAsync->shouldHaveBeenCalledTimes(count($webhooks)); $logWarning->shouldHaveBeenCalledTimes(count($invalidWebhooks)); } public function provideVisits(): iterable { yield 'regular visit' => [ Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance()), ['shortUrl', 'visit'], ]; yield 'orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), ['visit'],]; } private function createListener(array $webhooks, bool $notifyOrphanVisits = true): NotifyVisitToWebHooks { return new NotifyVisitToWebHooks( $this->httpClient->reveal(), $this->em->reveal(), $this->logger->reveal(), new WebhookOptions( ['webhooks' => $webhooks, 'notify_orphan_visits_to_webhooks' => $notifyOrphanVisits], ), new ShortUrlDataTransformer(new ShortUrlStringifier([])), new AppOptions('Shlink', '1.2.3'), ); } }