Some tests & implementation around verifying and parsing received webmentions

This commit is contained in:
Lewis Dale 2023-03-14 08:27:46 +00:00
parent 83ec00c8a0
commit 38885beb74
4 changed files with 142 additions and 9 deletions

View File

@ -2,19 +2,39 @@
namespace Lewisdale\Webmentions; namespace Lewisdale\Webmentions;
use League\Uri\Exceptions\SyntaxError;
use Lewisdale\Webmentions\Exceptions\InvalidTargetException; use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
use Lewisdale\Webmentions\Exceptions\InvalidUrlException; use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
use Lewisdale\Webmentions\Gateways\WebmentionGatewayInterface; use Lewisdale\Webmentions\Gateways\WebmentionGatewayInterface;
use Lewisdale\Webmentions\Models\Webmention; use Lewisdale\Webmentions\Models\Webmention;
use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\HttpClient;
use League\Uri\Uri;
use Lewisdale\Webmentions\Exceptions\SourceNotFoundException;
use Lewisdale\Webmentions\Exceptions\TargetNotMentionedException;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class Endpoint { class Endpoint {
private readonly WebmentionGatewayInterface $gateway; private readonly WebmentionGatewayInterface $gateway;
public function validateUrl(string $url) : bool { function __construct(
return !!filter_var($url, FILTER_VALIDATE_URL); private readonly HttpClientInterface $httpClient
} )
{}
public function validateUrl(string $url) : bool {
try {
$uri = Uri::createFromString($url);
$scheme = $uri->getScheme();
$schemeValid = in_array($scheme, ["http", "https"]);
return $schemeValid && !!filter_var($url, FILTER_VALIDATE_URL);
} catch (SyntaxError $e)
{
return false;
}
}
public function receiveWebmention(string $source, string $target) : void public function receiveWebmention(string $source, string $target) : void
{ {
// Validate that both source and target are actual domains // Validate that both source and target are actual domains
@ -29,21 +49,29 @@ class Endpoint {
} }
// Parse content from the source // Parse content from the source
$client = HttpClient::create(); $response = $this->httpClient->request('GET', $source);
$response = $client->request('GET', $source);
if ($response->getStatusCode() === 200) if ($response->getStatusCode() < 400)
{ {
$content = $this->parseContent($response->getContent()); $content = $this->parseContent($response->getContent(), $target);
$author = $this->parseAuthor($response->getContent()); $author = $this->parseAuthor($response->getContent());
$webmention = new Webmention(null, $target, $source, $content, $author); $webmention = new Webmention(null, $target, $source, $content, $author);
$this->gateway->save($webmention); $this->gateway->save($webmention);
} else {
throw new SourceNotFoundException();
} }
} }
private function parseContent(string $content) : ?string private function parseContent(string $content, string $target) : ?string
{ {
$body = new Crawler($content);
$anchors = $body->filter('a[href="'. $target . '"]');
if (!$anchors->count()) {
throw new TargetNotMentionedException();
}
return null; return null;
} }

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Exceptions;
use Exception;
class SourceNotFoundException extends Exception {}
?>

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Exceptions;
use Exception;
class TargetNotMentionedException extends Exception {}
?>

View File

@ -1,16 +1,105 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
use Lewisdale\Webmentions\Endpoint; 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 PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\TestWith;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class EndpointTest extends TestCase { class EndpointTest extends TestCase {
#[TestWith(["https://my.url.com", true])] #[TestWith(["https://my.url.com", true])]
#[TestWith(["my.url.com", false])] #[TestWith(["my.url.com", false])]
public function testValidatesUrls(string $url, bool $expected) public function testValidatesUrls(string $url, bool $expected)
{ {
$endpoint = new Endpoint(); $endpoint = new Endpoint($this->createMock(HttpClientInterface::class));
$this->assertEquals($expected, $endpoint->validateUrl($url), "Expected $url"); $this->assertEquals($expected, $endpoint->validateUrl($url), "Expected $url");
} }
#[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);
$endpoint = new Endpoint($this->createMock(HttpClientInterface::class));
$endpoint->receiveWebmention($source, $target);
}
public function testThrowsInvalidTargetExceptionIfTheTargetUrlIsNotOnCurrentSite()
{
$this->expectException(InvalidTargetException::class);
$endpoint = new Endpoint($this->createMock(HttpClientInterface::class));
$endpoint->receiveWebmention("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";
$mockClient = $this->createMock(HttpClientInterface::class);
$mockResponse = $this->createMock(ResponseInterface::class);
$mockClient->expects($this->once())
->method('request')
->with($this->identicalTo('GET'), $this->identicalTo($source))
->will($this->returnValue($mockResponse));
$mockResponse->method('getStatusCode')
->will($this->returnValue($statusCode));
$this->expectException(SourceNotFoundException::class);
$endpoint = new Endpoint($mockClient);
$endpoint->receiveWebmention($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;
$mockClient = $this->createMock(HttpClientInterface::class);
$mockResponse = $this->createMock(ResponseInterface::class);
$mockClient->expects($this->once())
->method('request')
->with($this->identicalTo('GET'), $this->identicalTo($source))
->will($this->returnValue($mockResponse));
$mockResponse->method('getStatusCode')
->will($this->returnValue(200));
$mockResponse->method('getContent')
->willReturn($content);
$this->expectException(TargetNotMentionedException::class);
$endpoint = new Endpoint($mockClient);
$endpoint->receiveWebmention($source, $target);
}
} }
?> ?>