<?php declare(strict_types=1); use Lewisdale\Webmentions\Endpoint; use Lewisdale\Webmentions\Exceptions\InvalidTargetException; use Lewisdale\Webmentions\Exceptions\InvalidUrlException; use Lewisdale\Webmentions\Exceptions\SourceNotFoundException; use Lewisdale\Webmentions\Exceptions\TargetNotMentionedException; use Lewisdale\Webmentions\Gateways\WebmentionGatewayInterface; use Lewisdale\Webmentions\Models\MentionType; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; class EndpointTest extends TestCase { private HttpClientInterface $mockClient; private ResponseInterface $mockResponse; private WebmentionGatewayInterface $mockGateway; protected function setUp(): void { $this->mockClient = $this->createMock(HttpClientInterface::class); $this->mockResponse = $this->createMock(ResponseInterface::class); $this->mockGateway = $this->createMock(WebmentionGatewayInterface::class); } private function objectContains(string $key, mixed $expected) { return $this->callback(function (object $obj) use ($expected, $key) { $val = $obj->$key; $type = gettype($val); return match ($type) { "object" => $val == $expected, "array" => count(array_diff($val, $expected)) === 0, default => $val === $expected }; }); } private function callEndpoint(string $source, string $target) { $this->mockClient->method('request') ->with($this->identicalTo('GET'), $this->identicalTo($source)) ->will($this->returnValue($this->mockResponse)); $endpoint = new Endpoint($this->mockClient, $this->mockGateway); $endpoint->receiveWebmention($source, $target); } #[TestWith(["https://my-valid-source.url", "htt://my-invalid-target.com"])] #[TestWith(["my-invalid-source", "https://my-valid-target.com"])] #[TestWith(["http:///an-invalid-source", "an-invalid-target"])] public function testThrowsInvalidUrlExceptionIfTheUrlIsInvalid(string $source, string $target) { $this->expectException(InvalidUrlException::class); $this->callEndpoint($source, $target); } public function testThrowsInvalidTargetExceptionIfTheTargetUrlIsNotOnCurrentSite() { $this->expectException(InvalidTargetException::class); $this->callEndpoint("https://a-valid-source.url", "https://not-my-site.com"); } #[TestWith([404])] #[TestWith([401])] #[TestWith([500])] #[TestWith([501])] #[TestWith([502])] #[TestWith([503])] #[TestWith([500])] public function testItShouldThrowASourceNotFoundExceptionIfTheSourceUrlIsUnreachable(int $statusCode) { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $this->mockClient->expects($this->once()) ->method('request') ->with($this->identicalTo('GET'), $this->identicalTo($source)) ->will($this->returnValue($this->mockResponse)); $this->mockResponse->method('getStatusCode') ->will($this->returnValue($statusCode)); $this->expectException(SourceNotFoundException::class); $this->callEndpoint($source, $target); } public function testItShouldThrowATargetNotMentionedErrorIfTheSourceDoesNotMentionTheTarget() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<XML <html> <head> </head> <body> <h1>Some content</h1> <p>Here's some body content. It <a href="/another/page">contains a url</a>.</p> <p>But it does not mention the target.</p> </body> </html> XML; $this->mockResponse->method('getStatusCode') ->will($this->returnValue(200)); $this->mockResponse->method('getContent') ->willReturn($content); $this->expectException(TargetNotMentionedException::class); $this->callEndpoint($source, $target); } private function verifyContent(string $source, string $target, string $content, \PHPUnit\Framework\Constraint\Callback $verify) { $this->mockResponse->method('getStatusCode') ->will($this->returnValue(200)); $this->mockResponse->method('getContent') ->willReturn($content); $this->mockGateway->expects($this->once()) ->method('save') ->with($verify); $this->callEndpoint($source, $target); } public function testItShouldParseAWebmentionAsALikeIfItHasTheCorrectMicroFormat() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<XML <html> <head> </head> <body> <article> <h1>Some content</h1> <p>Here's some body content. It <a href="/another/page">contains a url</a>.</p> <p>I'm liking <a href="$target" class="u-like-of">this post</a>.</p> </article> </body> </html> XML; $this->verifyContent($source, $target, $content, $this->objectContains('type', MentionType::Like)); } public function testItShouldParseAWebmentionAsAMentionIfItHasNoMicroformat() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<XML <html> <head> </head> <body> <article class="h-entry"> <h1>Some content</h1> <p>Here's some body content. It <a href="/another/page">contains a url</a>.</p> <p>I'm writing about <a href="$target" class="my-link">this post</a>.</p> </article> </body> </html> XML; $this->verifyContent($source, $target, $content, $this->objectContains('type', MentionType::Mention)); } public function testItShouldParseAWebmentionAsAReplyIfItHasTheCorrectMicroFormat() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<XML <html> <head> </head> <body> <article class="h-entry"> <h1>Some content</h1> <p>Here's some body content. It <a href="/another/page">contains a url</a>.</p> <p>I'm writing about <a href="$target" class="u-in-reply-to">this post</a>.</p> </article> </body> </html> XML; $this->verifyContent($source, $target, $content, $this->objectContains('type', MentionType::Reply)); } public function testItShouldParseAWebmentionAsARepost() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<XML <html> <head> </head> <body> <article="h-entry"> <h1>Some content</h1> <p>Here's some body content. It <a href="/another/page">contains a url</a>.</p> <p>I'm writing about <a href="$target" class="u-repost-of">this post</a>.</p> </article> </body> </html> XML; $this->verifyContent($source, $target, $content, $this->objectContains('type', MentionType::Repost)); } public function testItShouldParseARepostsContent() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<XML <html> <head> </head> <body> <article="h-entry"> <h1>Some content</h1> <p>Here's some body content. It <a href="/another/page">contains a url</a>.</p> <p>I'm writing about <a href="$target" class="u-repost-of">this post</a>.</p> </article> </body> </html> XML; $this->verifyContent($source, $target, $content, $this->objectContains('content', "Reposted this post")); } public function testItShouldParseALikeContent() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<HTML <html> <head> </head> <body> <span class="h-entry"> <a class="u-like-of" href="$target">A Cool Post</a><a class="u-url" href="/"></a> </span> </body> </html> HTML; $this->verifyContent($source, $target, $content, $this->objectContains('content', "Liked this post")); } public function testItShouldParseAReplyContent() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<HTML <html> <head> </head> <body> <article class="h-entry"> <a class="u-in-reply-to" rel="in-reply-to" href="$target">@post</a>: That's a great idea!<a class="u-url" href="/"></a> </article> </body> </html> HTML; $this->verifyContent($source, $target, $content, $this->objectContains('content', "@post: That's a great idea!")); } public function testItShouldParseAnAuthorCardWithANameUrlAndPhoto() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<HTML <html> <head> </head> <body> <article class="h-entry"> <a class="u-in-reply-to" rel="in-reply-to" href="$target">@post</a>: That's a great idea!<a class="u-url" href="/"></a> <div class="h-card"> <p class="p-name">Anne Author</p> who can be found at <a class="u-url" href="https://my-blog.com">my-blog.com</a>. <img src="https://dummyimage.com/100x100/fff/aaa" class="u-photo" alt="My profile picture" /> </div> </article> </body> </html> HTML; $expected = new \Lewisdale\Webmentions\Models\Author(null, "Anne Author", "https://my-blog.com", "https://dummyimage.com/100x100/fff/aaa"); $this->verifyContent($source, $target, $content, $this->objectContains('author', $expected)); } public function testItShouldParseAnAuthorCardWithAMinimalHCard() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<HTML <html> <head> </head> <body> <article class="h-entry"> <a class="u-in-reply-to" rel="in-reply-to" href="$target">@post</a>: That's a great idea!<a class="u-url" href="/"></a> <span class="h-card"> <a class="p-name p-org u-url" href="https://microformats.org/">microformats.org</a> </span> </article> </body> </html> HTML; $expected = new \Lewisdale\Webmentions\Models\Author( null, "microformats.org", "https://microformats.org/", "" ); $this->verifyContent($source, $target, $content, $this->objectContains('author', $expected)); } public function testItShouldParseAnAuthorCardWithAVeryMinimalHCard() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<HTML <html> <head> </head> <body> <article class="h-entry"> <a class="u-in-reply-to" rel="in-reply-to" href="$target">@post</a>: That's a great idea!<a class="u-url" href="/"></a> <a class="h-card" href="https://microformats.org/">microformats.org</a> </article> </body> </html> HTML; $expected = new \Lewisdale\Webmentions\Models\Author( null, "microformats.org", "https://microformats.org/", "" ); $this->verifyContent($source, $target, $content, $this->objectContains('author', $expected)); } public function testItShouldParseAnAuthorCardOutsideTheEntrry() { $source = "https://my-valid-source-url.com"; $target = "https://lewisdale.dev/post/a-post-page"; $content = <<<HTML <html> <head> </head> <body> <article class="h-entry"> <a class="u-in-reply-to" rel="in-reply-to" href="$target">@post</a>: That's a great idea!<a class="u-url" href="/"></a> </article> <div class="h-card"> <p class="p-name">Anne Author</p> who can be found at <a class="u-url" href="https://my-blog.com">my-blog.com</a>. <img src="https://dummyimage.com/100x100/fff/aaa" class="u-photo" alt="My profile picture" /> </div> </body> </html> HTML; $expected = new \Lewisdale\Webmentions\Models\Author( null, "Anne Author", "https://my-blog.com", "https://dummyimage.com/100x100/fff/aaa" ); $this->verifyContent($source, $target, $content, $this->objectContains('author', $expected)); } }