@@ -74,7 +74,7 @@
diff --git a/extension.php b/extension.php
index fe9a8b0..80d7d97 100644
--- a/extension.php
+++ b/extension.php
@@ -2,20 +2,15 @@
declare(strict_types=1);
-use RedditImage\Transformer\DisplayTransformer;
-use RedditImage\Transformer\InsertTransformer;
+use RedditImage\Client\Client;
+use RedditImage\Processor\BeforeInsertProcessor;
+use RedditImage\Processor\BeforeDisplayProcessor;
+use RedditImage\Settings;
class RedditImageExtension extends Minz_Extension {
- private const DEFAULT_IMAGEHEIGHT = 70;
- private const DEFAULT_MUTEDVIDEO = true;
- private const DEFAULT_DISPLAYIMAGE = true;
- private const DEFAULT_DISPLAYVIDEO = true;
- private const DEFAULT_DISPLAYORIGINAL = true;
- private const DEFAULT_DISPLAYMETADATA = false;
- private const DEFAULT_DISPLAYTHUMBNAILS = false;
-
- private DisplayTransformer $displayTransformer;
- private InsertTransformer $insertTransformer;
+ private BeforeDisplayProcessor $beforeDisplayProcessor;
+ private BeforeInsertProcessor $beforeInsertProcessor;
+ public Settings $settings;
public function autoload($class_name): void {
if (0 === strpos($class_name, 'RedditImage')) {
@@ -24,9 +19,14 @@ public function autoload($class_name): void {
}
}
+ private function getUserAgent(): string {
+ return "{$this->getName()}/{$this->getVersion()} by {$this->getAuthor()}";
+ }
+
public function init(): void {
spl_autoload_register([$this, 'autoload']);
+ define('REDDITIMAGE_VERSION', $this->getVersion());
$this->registerTranslates();
$current_user = Minz_Session::param('currentUser');
@@ -37,11 +37,13 @@ public function init(): void {
Minz_View::appendStyle($this->getFileUrl($filename, 'css'));
}
- $this->displayTransformer = new DisplayTransformer($this->getDisplayImage(), $this->getDisplayVideo(), $this->getMutedVideo(), $this->getDisplayOriginal(), $this->getDisplayMetadata(), $this->getDisplayThumbnails());
- $this->insertTransformer = new InsertTransformer($this->getImgurClientId());
+ $this->settings = new Settings($this->getUserConfiguration());
- $this->registerHook('entry_before_display', [$this->displayTransformer, 'transform']);
- $this->registerHook('entry_before_insert', [$this->insertTransformer, 'transform']);
+ $this->beforeDisplayProcessor = new BeforeDisplayProcessor($this->settings);
+ $this->beforeInsertProcessor = new BeforeInsertProcessor($this->settings, new Client($this->getUserAgent()));
+
+ $this->registerHook('entry_before_display', [$this->beforeDisplayProcessor, 'process']);
+ $this->registerHook('entry_before_insert', [$this->beforeInsertProcessor, 'process']);
}
public function handleConfigureAction(): void {
@@ -51,7 +53,7 @@ public function handleConfigureAction(): void {
if (Minz_Request::isPost()) {
$configuration = [
- 'imageHeight' => (int) Minz_Request::param('image-height', static::DEFAULT_IMAGEHEIGHT),
+ 'imageHeight' => (int) Minz_Request::param('image-height', $this->settings->getDefaultImageHeight()),
'mutedVideo' => (bool) Minz_Request::param('muted-video'),
'displayImage' => (bool) Minz_Request::param('display-image'),
'displayVideo' => (bool) Minz_Request::param('display-video'),
@@ -67,36 +69,4 @@ public function handleConfigureAction(): void {
);
}
}
-
- public function getImageHeight(): int {
- return $this->getUserConfigurationValue('imageHeight', static::DEFAULT_IMAGEHEIGHT);
- }
-
- public function getMutedVideo(): bool {
- return $this->getUserConfigurationValue('mutedVideo', static::DEFAULT_MUTEDVIDEO);
- }
-
- public function getDisplayImage(): bool {
- return $this->getUserConfigurationValue('displayImage', static::DEFAULT_DISPLAYIMAGE);
- }
-
- public function getDisplayVideo(): bool {
- return $this->getUserConfigurationValue('displayVideo', static::DEFAULT_DISPLAYVIDEO);
- }
-
- public function getDisplayOriginal(): bool {
- return $this->getUserConfigurationValue('displayOriginal', static::DEFAULT_DISPLAYORIGINAL);
- }
-
- public function getDisplayMetadata(): bool {
- return $this->getUserConfigurationValue('displayMetadata', static::DEFAULT_DISPLAYMETADATA);
- }
-
- public function getDisplayThumbnails(): bool {
- return $this->getUserConfigurationValue('displayThumbnails', static::DEFAULT_DISPLAYTHUMBNAILS);
- }
-
- public function getImgurClientId(): string {
- return $this->getUserConfigurationValue('imgurClientId', '');
- }
}
diff --git a/metadata.json b/metadata.json
index cf3cc3c..5e0d939 100644
--- a/metadata.json
+++ b/metadata.json
@@ -2,7 +2,7 @@
"name": "Reddit Image",
"author": "Alexis Degrugillier",
"description": "Modify reddit entries by displaying known medias and easing access to the linked resource",
- "version": "0.12.5",
+ "version": "1.0.0",
"entrypoint": "RedditImage",
"type": "user"
}
diff --git a/phpunit.xml b/phpunit.xml.dist
similarity index 57%
rename from phpunit.xml
rename to phpunit.xml.dist
index 96240dd..0e9880f 100644
--- a/phpunit.xml
+++ b/phpunit.xml.dist
@@ -8,10 +8,19 @@
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
- failOnWarning="true">
+ failOnWarning="true"
+ displayDetailsOnTestsThatTriggerErrors="true"
+ displayDetailsOnTestsThatTriggerNotices="true"
+ displayDetailsOnTestsThatTriggerDeprecations="true"
+ displayDetailsOnTestsThatTriggerWarnings="true"
+ displayDetailsOnSkippedTests="true">
tests
+ tests/Client/ClientTest.php
+
+
+ tests/Client/ClientTest.php
@@ -19,4 +28,7 @@
.
+
+
+
diff --git a/tests/Client/ClientTest.php b/tests/Client/ClientTest.php
new file mode 100644
index 0000000..75e3b2d
--- /dev/null
+++ b/tests/Client/ClientTest.php
@@ -0,0 +1,135 @@
+client = new Client('Reddit Image/test by Alexis Degrugillier');
+ }
+
+ /**
+ * @testWith ["https://example.org", true]
+ * ["https://github.com/aledeg/xExtension-RedditImage/404", false]
+ */
+ public function testIsAccessible(string $url, bool $isAccessible): void {
+ $this->assertEquals($isAccessible, $this->client->isAccessible($url));
+ }
+
+ public function testJsonGetGfycatVideo(): void {
+ $json = $this->client->jsonGet('https://api.gfycat.com/v1/gfycats/sandytatteredakitainu');
+ $this->assertIsArray($json);
+
+ $this->assertEquals('https://giant.gfycat.com/SandyTatteredAkitainu.mp4', $json['gfyItem']['mp4Url'] ?? null);
+ $this->assertEquals('https://giant.gfycat.com/SandyTatteredAkitainu.webm', $json['gfyItem']['webmUrl'] ?? null);
+ }
+
+ public function testJsonGetImgurGalleryWithClientId(): void {
+ if (!defined('IMGUR_CLIENT_ID')) {
+ $this->markTestSkipped('Imgur client ID is not defined in the configuration file');
+ }
+
+ $clientId = IMGUR_CLIENT_ID;
+ $json = $this->client->jsonGet('https://api.imgur.com/3/album/JOh3bhn/images', [
+ "Authorization: Client-ID {$clientId}",
+ ]);
+ $this->assertIsArray($json);
+
+ $metadata = $json['data'];
+ $this->assertIsArray($metadata);
+ $this->assertCount(14, $metadata);
+
+ $links = array_column($metadata, 'link');
+ $this->assertIsArray($links);
+ $this->assertCount(14, $links);
+ $this->assertEquals([
+ 'https://i.imgur.com/kHJbvmh.png',
+ 'https://i.imgur.com/M5XwH7K.png',
+ 'https://i.imgur.com/nIAIjJO.png',
+ 'https://i.imgur.com/ASyDzpc.png',
+ 'https://i.imgur.com/aMQ00r6.png',
+ 'https://i.imgur.com/F6mnogZ.png',
+ 'https://i.imgur.com/hGgJ4vX.png',
+ 'https://i.imgur.com/tnd6dKW.png',
+ 'https://i.imgur.com/Bi1mSnq.png',
+ 'https://i.imgur.com/4mIdYn5.png',
+ 'https://i.imgur.com/HcburKp.png',
+ 'https://i.imgur.com/dtiq3CN.png',
+ 'https://i.imgur.com/07byVN9.png',
+ 'https://i.imgur.com/U07O5Co.png',
+ ], $links);
+ }
+
+ public function testJsonGetRedditGallery(): void {
+ $json = $this->client->jsonGet('https://www.reddit.com/r/quilting/comments/13ajhjf/lochness_monster_fpp_block/.json');
+ $this->assertIsArray($json);
+
+ $metadata = $json[0]['data']['children'][0]['data']['media_metadata'];
+ $this->assertIsArray($metadata);
+
+ $this->assertCount(7, $metadata);
+ $this->assertEquals([
+ 'm8do0y12wdya1',
+ 'fvbthx12wdya1',
+ 'nb0y7y12wdya1',
+ '2bel5y12wdya1',
+ 'chnqmy12wdya1',
+ '9w7sby12wdya1',
+ 'rgww3y12wdya1',
+ ], array_keys($metadata));
+ foreach ($metadata as $values) {
+ $this->assertArrayHasKey('m', $values);
+ $this->assertEquals('image/jpg', $values['m']);
+ }
+ }
+
+ public function testJsonGetRedditGalleryWithCrosspost(): void {
+ $json = $this->client->jsonGet('https://www.reddit.com/r/LegendsOfRuneterra/comments/l2k33y/might_be_an_obvious_connection_but_i_just_noticed/.json');
+ $this->assertIsArray($json);
+
+ $metadata = $json[0]['data']['children'][0]['data']['crosspost_parent_list'][0]['media_metadata'];
+ $this->assertCount(4, $metadata);
+ $this->assertEquals([
+ 'xpt2x73houc61',
+ '5lztrcniouc61',
+ 'fnwszdajouc61',
+ '9wq3m82iouc61',
+ ], array_keys($metadata));
+ foreach ($metadata as $values) {
+ $this->assertArrayHasKey('m', $values);
+ $this->assertEquals('image/jpg', $values['m']);
+ }
+ }
+
+ public function testJsonGetRedditVideo(): void {
+ $json = $this->client->jsonGet('https://www.reddit.com/r/oddlyterrifying/comments/13ommr2/while_some_find_a_baby_stingray_video_shoot/.json');
+ $this->assertIsArray($json);
+
+ $metadata = $json[0]['data']['children'][0]['data']['media']['reddit_video']['fallback_url'] ?? null;
+ $this->assertEquals('https://v.redd.it/nvcnk4qsee1b1/DASH_1080.mp4?source=fallback', $metadata);
+ }
+
+ public function testJsonGetRedditVideoWithCrosspost(): void {
+ $json = $this->client->jsonGet('https://www.reddit.com/r/hiddenrooms/comments/11vohr2/crosspost_from_rcarpentry_throwback_to_a_really/.json');
+ $this->assertIsArray($json);
+
+ $metadata = $json[0]['data']['children'][0]['data']['crosspost_parent_list'][0]['media']['reddit_video']['fallback_url'] ?? null;
+ $this->assertEquals('https://v.redd.it/a9ticvrffpoa1/DASH_1080.mp4?source=fallback', $metadata);
+ }
+}
diff --git a/tests/Media/ImageTest.php b/tests/Media/ImageTest.php
index a9a3618..9c594fd 100644
--- a/tests/Media/ImageTest.php
+++ b/tests/Media/ImageTest.php
@@ -17,7 +17,6 @@ public function test(): void {
$media = new Image('https://example.org');
$this->assertInstanceOf(DomElementInterface::class, $media);
- $this->assertEquals('https://example.org', $media->getUrl());
$domElement = m::mock(\DomElement::class);
$domElement->expects('setAttribute')
diff --git a/tests/Media/LinkTest.php b/tests/Media/LinkTest.php
new file mode 100644
index 0000000..05e2727
--- /dev/null
+++ b/tests/Media/LinkTest.php
@@ -0,0 +1,53 @@
+assertInstanceOf(DomElementInterface::class, $media);
+
+ $textNode = m::mock(\DomText::class);
+
+ $aElement = m::mock(\DomElement::class);
+ $aElement->expects('setAttribute')
+ ->once()
+ ->with('href', 'https://example.org');
+ $aElement->expects('appendChild')
+ ->once()
+ ->with($textNode);
+
+ $pElement = m::mock(\DomElement::class);
+ $pElement->expects('appendChild')
+ ->once()
+ ->with($aElement)
+ ->andReturns($aElement);
+
+ $domDocument = m::mock(\DomDocument::class);
+ $domDocument->expects('createElement')
+ ->once()
+ ->with('p')
+ ->andReturns($pElement);
+ $domDocument->expects('createElement')
+ ->once()
+ ->with('a')
+ ->andReturns($aElement);
+ $domDocument->expects('createTextNode')
+ ->once()
+ ->with('https://example.org')
+ ->andReturns($textNode);
+
+ $this->assertEquals($pElement, $media->toDomElement($domDocument));
+ }
+}
diff --git a/tests/PHPUnit/Constraint/htmlHasGeneratedContentContainer.php b/tests/PHPUnit/Constraint/htmlHasGeneratedContentContainer.php
new file mode 100644
index 0000000..7365143
--- /dev/null
+++ b/tests/PHPUnit/Constraint/htmlHasGeneratedContentContainer.php
@@ -0,0 +1,37 @@
+loadHTML($other, LIBXML_NOERROR) === false) {
+ return false;
+ }
+
+ $xpath = new \DOMXpath($dom);
+ $container = $xpath->query("body/div[@class='reddit-image figure']");
+ if ($container->length !== 1) {
+ return false;
+ }
+
+ $comment = $container->item(0)->firstChild;
+ if ($comment->nodeType !== XML_COMMENT_NODE) {
+ return false;
+ }
+
+ return preg_match('#xExtension-RedditImage/\w+ \| \w+ \| RedditImage\\\\Transformer\\\\.+Transformer#', $comment->textContent) === 1;
+ }
+
+ public function toString(): string {
+ return 'has a content container';
+ }
+}
diff --git a/tests/PHPUnit/Constraint/htmlHasImage.php b/tests/PHPUnit/Constraint/htmlHasImage.php
new file mode 100644
index 0000000..f6f0cf1
--- /dev/null
+++ b/tests/PHPUnit/Constraint/htmlHasImage.php
@@ -0,0 +1,39 @@
+imageUrl = $imageUrl;
+ }
+
+ public function matches($other): bool {
+ if (!is_string($other)) {
+ return false;
+ }
+
+ $dom = new \DomDocument('1.0', 'UTF-8');
+ if ($dom->loadHTML($other, LIBXML_NOERROR) === false) {
+ return false;
+ }
+
+ $xpath = new \DOMXpath($dom);
+ $images = $xpath->query("body/div/img[@class='reddit-image'][@src='{$this->imageUrl}']");
+
+ if ($images->length !== 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function toString(): string {
+ return "has the image with {$this->imageUrl} source";
+ }
+}
diff --git a/tests/PHPUnit/Constraint/htmlHasLink.php b/tests/PHPUnit/Constraint/htmlHasLink.php
new file mode 100644
index 0000000..efc59a0
--- /dev/null
+++ b/tests/PHPUnit/Constraint/htmlHasLink.php
@@ -0,0 +1,39 @@
+link = $link;
+ }
+
+ public function matches($other): bool {
+ if (!is_string($other)) {
+ return false;
+ }
+
+ $dom = new \DomDocument('1.0', 'UTF-8');
+ if ($dom->loadHTML($other, LIBXML_NOERROR) === false) {
+ return false;
+ }
+
+ $xpath = new \DOMXpath($dom);
+ $links = $xpath->query("body/div/p/a[@href='{$this->link}']");
+
+ if ($links->length !== 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function toString(): string {
+ return "has the link {$this->imageUrl}";
+ }
+}
diff --git a/tests/PHPUnit/Constraint/htmlHasVideo.php b/tests/PHPUnit/Constraint/htmlHasVideo.php
new file mode 100644
index 0000000..14d8b9d
--- /dev/null
+++ b/tests/PHPUnit/Constraint/htmlHasVideo.php
@@ -0,0 +1,41 @@
+format = $format;
+ $this->videoUrl = $videoUrl;
+ }
+
+ public function matches($other): bool {
+ if (!is_string($other)) {
+ return false;
+ }
+
+ $dom = new \DomDocument('1.0', 'UTF-8');
+ if ($dom->loadHTML($other, LIBXML_NOERROR) === false) {
+ return false;
+ }
+
+ $xpath = new \DOMXpath($dom);
+ $videos = $xpath->query("body/div/video[@class='reddit-image'][@controls='true'][@preload='metadata']/source[@src='{$this->videoUrl}'][@type='video/{$this->format}']");
+
+ if ($videos->length !== 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function toString(): string {
+ return "has the {$this->format} video with {$this->videoUrl} source";
+ }
+}
diff --git a/tests/PHPUnit/Constraint/htmlHasVideoWithAudio.php b/tests/PHPUnit/Constraint/htmlHasVideoWithAudio.php
new file mode 100644
index 0000000..a6c149f
--- /dev/null
+++ b/tests/PHPUnit/Constraint/htmlHasVideoWithAudio.php
@@ -0,0 +1,49 @@
+format = $format;
+ $this->videoUrl = $videoUrl;
+ $this->audioUrl = $audioUrl;
+ }
+
+ public function matches($other): bool {
+ if (!is_string($other)) {
+ return false;
+ }
+
+ $dom = new \DomDocument('1.0', 'UTF-8');
+ if ($dom->loadHTML($other, LIBXML_NOERROR) === false) {
+ return false;
+ }
+
+ $xpath = new \DOMXpath($dom);
+ $videos = $xpath->query("body/div/video[@class='reddit-image'][@controls='true'][@preload='metadata']/source[@src='{$this->videoUrl}'][@type='video/{$this->format}']");
+
+ if ($videos->length !== 1) {
+ return false;
+ }
+
+ $audios = $xpath->query("body/div/video/audio[@controls='true']/source[@src='{$this->audioUrl}']");
+
+ if ($audios->length !== 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function toString(): string {
+ return "has the {$this->format} video with {$this->videoUrl} source and {$this->audioUrl} audio";
+ }
+}
diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php
new file mode 100644
index 0000000..36c4da4
--- /dev/null
+++ b/tests/PHPUnit/TestCase.php
@@ -0,0 +1,38 @@
+assertFalse($settings->hasImgurClientId());
+ $this->assertEquals('', $settings->getImgurClientId());
+ $this->assertEquals(70, $settings->getDefaultImageHeight());
+ $this->assertEquals(70, $settings->getImageHeight());
+ $this->assertTrue($settings->getMutedVideo());
+ $this->assertTrue($settings->getDisplayImage());
+ $this->assertTrue($settings->getDisplayVideo());
+ $this->assertTrue($settings->getDisplayOriginal());
+ $this->assertFalse($settings->getDisplayMetadata());
+ $this->assertFalse($settings->getDisplayThumbnails());
+ }
+
+ public function testWhenSettings(): void
+ {
+ $settings = new Settings([
+ 'imgurClientId' => 'abc123def',
+ 'imageHeight' => 90,
+ 'mutedVideo' => false,
+ 'displayImage' => false,
+ 'displayVideo' => false,
+ 'displayOriginal' => false,
+ 'displayMetadata' => true,
+ 'displayThumbnails' => true,
+ ]);
+
+ $this->assertTrue($settings->hasImgurClientId());
+ $this->assertEquals('abc123def', $settings->getImgurClientId());
+ $this->assertEquals(70, $settings->getDefaultImageHeight());
+ $this->assertEquals(90, $settings->getImageHeight());
+ $this->assertFalse($settings->getMutedVideo());
+ $this->assertFalse($settings->getDisplayImage());
+ $this->assertFalse($settings->getDisplayVideo());
+ $this->assertFalse($settings->getDisplayOriginal());
+ $this->assertTrue($settings->getDisplayMetadata());
+ $this->assertTrue($settings->getDisplayThumbnails());
+ }
+
+ public function testProcessor(): void {
+ $settings = new Settings([]);
+
+ $this->assertEquals('no', $settings->getProcessor());
+
+ $settings->setProcessor('test');
+
+ $this->assertEquals('test', $settings->getProcessor());
+ }
+}
diff --git a/tests/Transformer/Agnostic/ImageTransformerTest.php b/tests/Transformer/Agnostic/ImageTransformerTest.php
new file mode 100644
index 0000000..e717411
--- /dev/null
+++ b/tests/Transformer/Agnostic/ImageTransformerTest.php
@@ -0,0 +1,92 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new ImageTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $link, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($link);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'JPG image without query string' => ['https://example.org/image.jpg', true];
+ yield 'JPEG image without query string' => ['https://example.org/image.jpeg', true];
+ yield 'PNG image without query string' => ['https://example.org/image.png', true];
+ yield 'BMP image without query string' => ['https://example.org/image.bmp', true];
+ yield 'GIF image without query string' => ['https://example.org/image.gif', true];
+ yield 'JPG image with query string' => ['https://example.org/image.jpg?key=value', true];
+ yield 'JPEG image with query string' => ['https://example.org/image.jpeg?key=value', true];
+ yield 'PNG image with query string' => ['https://example.org/image.png?key=value', true];
+ yield 'BMP image with query string' => ['https://example.org/image.bmp?key=value', true];
+ yield 'GIF image with query string' => ['https://example.org/image.gif?key=value', true];
+ yield 'JPG format in query string' => ['https://example.org/image?format=jpg', false];
+ yield 'JPEG format in query string' => ['https://example.org/image?format=jpeg', false];
+ yield 'PNG format in query string' => ['https://example.org/image?format=png', false];
+ yield 'BMP format in query string' => ['https://example.org/image?format=bmp', false];
+ yield 'GIF format in query string' => ['https://example.org/image?format=gif', false];
+ yield 'JPG as route section' => ['https://example.org/jpg', false];
+ yield 'JPEG as route section' => ['https://example.org/jpeg', false];
+ yield 'PNG as route section' => ['https://example.org/png', false];
+ yield 'BMP as route section' => ['https://example.org/bmp', false];
+ yield 'GIF as route section' => ['https://example.org/gif', false];
+ }
+
+ /**
+ * @dataProvider provideDataForTransform
+ */
+ public function testTransform(string $input, string $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($input);
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasImage($html, $expected);
+ }
+
+ public static function provideDataForTransform(): \Generator {
+ yield 'JPG image without query string' => ['https://example.org/image.jpg', 'https://example.org/image.jpg'];
+ yield 'JPEG image without query string' => ['https://example.org/image.jpeg', 'https://example.org/image.jpeg'];
+ yield 'PNG image without query string' => ['https://example.org/image.png', 'https://example.org/image.png'];
+ yield 'BMP image without query string' => ['https://example.org/image.bmp', 'https://example.org/image.bmp'];
+ yield 'GIF image without query string' => ['https://example.org/image.gif', 'https://example.org/image.gif'];
+ yield 'JPG image with query string' => ['https://example.org/image.jpg?key=value', 'https://example.org/image.jpg'];
+ yield 'JPEG image with query string' => ['https://example.org/image.jpeg?key=value', 'https://example.org/image.jpeg'];
+ yield 'PNG image with query string' => ['https://example.org/image.png?key=value', 'https://example.org/image.png'];
+ yield 'BMP image with query string' => ['https://example.org/image.bmp?key=value', 'https://example.org/image.bmp'];
+ yield 'GIF image with query string' => ['https://example.org/image.gif?key=value', 'https://example.org/image.gif'];
+ }
+}
diff --git a/tests/Transformer/Agnostic/LinkTransformerTest.php b/tests/Transformer/Agnostic/LinkTransformerTest.php
new file mode 100644
index 0000000..2bcfdd2
--- /dev/null
+++ b/tests/Transformer/Agnostic/LinkTransformerTest.php
@@ -0,0 +1,54 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new LinkTransformer($this->settings);
+ }
+
+ /**
+ * @testWith [true]
+ * [false]
+ */
+ public function testCanTransform(bool $hasRealContent): void {
+ $this->content->expects('hasReal')
+ ->once()
+ ->andReturns($hasRealContent);
+
+ $this->assertEquals(!$hasRealContent, $this->transformer->canTransform($this->content));
+ }
+
+ public function testTransform(): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://example.org');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasLink($html, 'https://example.org');
+ }
+}
diff --git a/tests/Transformer/Agnostic/VideoTransformerTest.php b/tests/Transformer/Agnostic/VideoTransformerTest.php
new file mode 100644
index 0000000..3857906
--- /dev/null
+++ b/tests/Transformer/Agnostic/VideoTransformerTest.php
@@ -0,0 +1,116 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new VideoTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $url, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'MP4 video without query string' => ['https://example.org/video.mp4', true];
+ yield 'WEBM video without query string' => ['https://example.org/video.webm', true];
+ yield 'MP4 video with query string' => ['https://example.org/video.mp4?key=value', false];
+ yield 'WEBM video with query string' => ['https://example.org/video.webm?key=value', false];
+ yield 'MP4 format in query string' => ['https://example.org/video?format=mp4', false];
+ yield 'WEBM format in query string' => ['https://example.org/video?format=webm', false];
+ yield 'MP4 as route section' => ['https://example.org/mp4', false];
+ yield 'WEBM as route section' => ['https://example.org/webm', false];
+ }
+
+ /**
+ * @testWith ["https://example.org/video.mp4"]
+ * ["https://example.org/video.webm"]
+ */
+ public function testTransformWithInaccessibleResource(string $url): void {
+ $client = m::mock(Client::class);
+ $client->expects('isAccessible')
+ ->once()
+ ->with($url)
+ ->andReturnFalse();
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+
+ $this->transformer->setClient($client);
+ $this->assertEquals('', $this->transformer->transform($this->content));
+ }
+
+ public function testTransformWithAccessibleMp4Resource(): void {
+ $url = 'https://example.org/video.mp4';
+
+ $client = m::mock(Client::class);
+ $client->expects('isAccessible')
+ ->once()
+ ->with($url)
+ ->andReturnTrue();
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasMp4Video($html, $url);
+ }
+
+ public function testTransformWithAccessibleWebmResource(): void {
+ $url = 'https://example.org/video.webm';
+
+ $client = m::mock(Client::class);
+ $client->expects('isAccessible')
+ ->once()
+ ->with($url)
+ ->andReturnTrue();
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasWebmVideo($html, $url);
+ }
+}
diff --git a/tests/Transformer/DisplayTransformerTest.php b/tests/Transformer/DisplayTransformerTest.php
deleted file mode 100644
index 8561a0a..0000000
--- a/tests/Transformer/DisplayTransformerTest.php
+++ /dev/null
@@ -1,12 +0,0 @@
-content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new VideoTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $url, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'not Gfycat URL' => ['https://example.org', false];
+ yield 'Gfycat without token' => ['https://gfycat.com/', false];
+ yield 'Gfycat with token' => ['https://gfycat.com/abc123', true];
+ yield 'Gfycat with "token" containing -' => ['https://gfycat.com/abc-123', false];
+ yield 'Gfycat with "token" containing .' => ['https://gfycat.com/abc.123', false];
+ yield 'Gfycat with token and route parameters' => ['https://gfycat.com/abc/def/ghi123', true];
+ }
+
+ public function testTransformWhenNoContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://api.gfycat.com/v1/gfycats/hello')
+ ->andReturns([]);
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://gfycat.com/hello');
+
+ $this->transformer->setClient($client);
+ $this->assertEquals('', $this->transformer->transform($this->content));
+ }
+
+ public function testTransformWhenContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://api.gfycat.com/v1/gfycats/hello')
+ ->andReturns([
+ 'gfyItem' => [
+ 'mp4Url' => 'https://gfycat.com/hello.mp4',
+ 'webmUrl' => 'https://gfycat.com/hello.webm',
+ ],
+ ]);
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://gfycat.com/hello');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasMp4Video($html, 'https://gfycat.com/hello.mp4');
+ $this->assertHtmlHasWebmVideo($html, 'https://gfycat.com/hello.webm');
+ }
+}
diff --git a/tests/Transformer/Imgur/GalleryWithClientIdTransformerTest.php b/tests/Transformer/Imgur/GalleryWithClientIdTransformerTest.php
new file mode 100644
index 0000000..361f303
--- /dev/null
+++ b/tests/Transformer/Imgur/GalleryWithClientIdTransformerTest.php
@@ -0,0 +1,122 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new GalleryWithClientIdTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $link, bool $hasClientId, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($link);
+ $this->settings->expects('hasImgurClientId')
+ ->once()
+ ->andReturns($hasClientId);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'client id, not an Imgur URL' => ['https://example.org', true, false];
+ yield 'client id, not an Imgur album URL' => ['https://imgur.com/image.png', false, false];
+ yield 'client id, Imgur album URL' => ['https://imgur.com/a/abc123', true, true];
+ yield 'client id, Imgur album URL with query string' => ['https://imgur.com/a/abc123?key=value', true, true];
+ yield 'no client id, not an Imgur URL' => ['https://example.org', false, false];
+ yield 'no client id, not an Imgur album URL' => ['https://imgur.com/image.png', false, false];
+ yield 'no client id, Imgur album URL' => ['https://imgur.com/a/abc123', false, false];
+ yield 'no client id, Imgur album URL with query string' => ['https://imgur.com/a/abc123?key=value', false, false];
+ }
+
+ public function testTransformWhenNoContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://api.imgur.com/3/album/hello/images', [
+ 'Authorization: Client-ID abc123',
+ ])
+ ->andReturns([]);
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://imgur.com/a/hello');
+ $this->settings->expects('getImgurClientId')
+ ->once()
+ ->andReturns('abc123');
+
+ $this->transformer->setClient($client);
+ $this->assertEquals('', $this->transformer->transform($this->content));
+ }
+
+ public function testTransformWhenContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://api.imgur.com/3/album/hello/images', [
+ 'Authorization: Client-ID abc123',
+ ])
+ ->andReturns([
+ 'data' => [
+ [
+ 'type' => 'video/mp4',
+ 'link' => 'https://example.org/hello.mp4',
+ ],
+ [
+ 'type' => 'image/jpg',
+ 'link' => 'https://example.org/hello.jpg',
+ ],
+ [
+ 'type' => 'video/webm',
+ 'link' => 'https://example.org/hello.webm',
+ ],
+ [
+ 'type' => 'image/png',
+ 'link' => 'https://example.org/hello.png',
+ ],
+ ],
+ ]);
+
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://imgur.com/a/hello');
+ $this->settings->expects('getImgurClientId')
+ ->once()
+ ->andReturns('abc123');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasImage($html, 'https://example.org/hello.png');
+ $this->assertHtmlHasImage($html, 'https://example.org/hello.jpg');
+ $this->assertHtmlHasMp4Video($html, 'https://example.org/hello.mp4');
+ $this->assertHtmlHasWebmVideo($html, 'https://example.org/hello.webm');
+ }
+}
diff --git a/tests/Transformer/Imgur/ImageTransformerTest.php b/tests/Transformer/Imgur/ImageTransformerTest.php
new file mode 100644
index 0000000..cc62779
--- /dev/null
+++ b/tests/Transformer/Imgur/ImageTransformerTest.php
@@ -0,0 +1,63 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new ImageTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $link, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($link);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'not an Imgur URL' => ['https://example.org/abc123', false];
+ yield 'Imgur URL' => ['https://imgur.com/', false];
+ yield 'Imgur URL with album token' => ['https://imgur.com/a/abc123', false];
+ yield 'Imgur URL with token' => ['https://imgur.com/abc123', true];
+ yield 'Imgur URL with query string' => ['https://imgur.com/abc123?key=value', false];
+ yield 'Imgur URL with terminal slash' => ['https://imgur.com/abc123/', false];
+ yield 'Imgur URL with extension' => ['https://imgur.com/abc123.png', false];
+ }
+
+ public function testTransform(): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://example.org/abc123');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasImage($html, 'https://example.org/abc123.png');
+ }
+}
diff --git a/tests/Transformer/Imgur/VideoTransformerTest.php b/tests/Transformer/Imgur/VideoTransformerTest.php
new file mode 100644
index 0000000..817c835
--- /dev/null
+++ b/tests/Transformer/Imgur/VideoTransformerTest.php
@@ -0,0 +1,61 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new VideoTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $url, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'not Imgur URL' => ['https://example.org', false];
+ yield 'Imgur URL without video' => ['https://imgur.com/image.jpg', false];
+ yield 'Imgur URL with video' => ['https://imgur.com/video.gifv', true];
+ yield 'Imgur URL with video and query string' => ['https://imgur.com/video.gifv?key=value', false];
+ yield 'Imgur URL with video and route parameter' => ['https://imgur.com/videos/video.gifv', false];
+ }
+
+ public function testTransform(): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns('https://imgur.com/video.gifv');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasMp4Video($html, 'https://imgur.com/video.mp4');
+ }
+}
diff --git a/tests/Transformer/InsertTransformerTest.php b/tests/Transformer/InsertTransformerTest.php
deleted file mode 100644
index 7b9ac3a..0000000
--- a/tests/Transformer/InsertTransformerTest.php
+++ /dev/null
@@ -1,12 +0,0 @@
-content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new GalleryTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $url, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'not Reddit URL' => ['https://example.org', false];
+ yield 'not Reddit gallery URL' => ['https://reddit.com', false];
+ yield 'Reddit gallery URL' => ['https://reddit.com/gallery', true];
+ }
+
+ public function testTransformWhenNoContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://example.org/.json')
+ ->andReturns([]);
+
+ $this->content->expects('getCommentsLink')
+ ->once()
+ ->andReturns('https://example.org/');
+
+ $this->transformer->setClient($client);
+ $this->assertEquals('', $this->transformer->transform($this->content));
+ }
+
+ public function testTransformWhenContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://example.org/.json')
+ ->andReturns([[
+ 'data' => [
+ 'children' => [[
+ 'data' => [
+ 'media_metadata' => [
+ 'image1of2' => [
+ 'm' => 'image/jpg',
+ ],
+ 'image2of2' => [
+ 'm' => 'image/jpg',
+ ],
+ ],
+ ],
+ ]],
+ ],
+ ]]);
+
+ $this->content->expects('getCommentsLink')
+ ->once()
+ ->andReturns('https://example.org/');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasImage($html, 'https://i.redd.it/image1of2.jpg');
+ $this->assertHtmlHasImage($html, 'https://i.redd.it/image2of2.jpg');
+ }
+
+ public function testTransformWhenCrosspostedContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://example.org/.json')
+ ->andReturns([[
+ 'data' => [
+ 'children' => [[
+ 'data' => [
+ 'crosspost_parent_list' => [[
+ 'media_metadata' => [
+ 'image1of2' => [
+ 'm' => 'image/jpg',
+ ],
+ 'image2of2' => [
+ 'm' => 'image/jpg',
+ ],
+ ],
+ ]],
+ ],
+ ]],
+ ],
+ ]]);
+
+ $this->content->expects('getCommentsLink')
+ ->once()
+ ->andReturns('https://example.org/');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasImage($html, 'https://i.redd.it/image1of2.jpg');
+ $this->assertHtmlHasImage($html, 'https://i.redd.it/image2of2.jpg');
+ }
+}
diff --git a/tests/Transformer/Reddit/VideoTransformerTest.php b/tests/Transformer/Reddit/VideoTransformerTest.php
new file mode 100644
index 0000000..08d8fac
--- /dev/null
+++ b/tests/Transformer/Reddit/VideoTransformerTest.php
@@ -0,0 +1,131 @@
+content = m::mock(Content::class);
+ $this->settings = m::mock(Settings::class);
+ $this->transformer = new VideoTransformer($this->settings);
+ }
+
+ /**
+ * @dataProvider provideDataForCanTransform
+ */
+ public function testCanTransform(string $url, bool $expected): void {
+ $this->content->expects('getContentLink')
+ ->once()
+ ->andReturns($url);
+
+ $this->assertEquals($expected, $this->transformer->canTransform($this->content));
+ }
+
+ public static function provideDataForCanTransform(): \Generator {
+ yield 'not Reddit URL' => ['https://example.org', false];
+ yield 'not Redd.it URL' => ['https://reddit.com', false];
+ yield 'not Redd.it video URL' => ['https://redd.it', false];
+ yield 'Redd.it video URL' => ['https://v.redd.it', true];
+ yield 'Redd.it video URL with query string' => ['https://v.redd.it?key=param', true];
+ }
+
+ public function testTransformWhenNoContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://example.org/.json')
+ ->andReturns([]);
+
+ $this->content->expects('getCommentsLink')
+ ->once()
+ ->andReturns('https://example.org/');
+
+ $this->transformer->setClient($client);
+ $this->assertEquals('', $this->transformer->transform($this->content));
+ }
+
+ public function testTransformWhenContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://example.org/.json')
+ ->andReturns([[
+ 'data' => [
+ 'children' => [[
+ 'data' => [
+ 'media' => [
+ 'reddit_video' => [
+ 'fallback_url' => 'https://example.org/DASH_video.mp4?source=fallback',
+ ],
+ ],
+ ],
+ ]],
+ ],
+ ]]);
+
+ $this->content->expects('getCommentsLink')
+ ->once()
+ ->andReturns('https://example.org/');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasMp4VideoWithAudio($html, 'https://example.org/DASH_video.mp4', 'https://example.org/DASH_audio.mp4');
+ }
+
+ public function testTransformWhenCrosspostedContent(): void {
+ $client = m::mock(Client::class);
+ $client->expects('jsonGet')
+ ->once()
+ ->with('https://example.org/.json')
+ ->andReturns([[
+ 'data' => [
+ 'children' => [[
+ 'data' => [
+ 'crosspost_parent_list' => [[
+ 'media' => [
+ 'reddit_video' => [
+ 'fallback_url' => 'https://example.org/DASH_video.mp4?source=fallback',
+ ],
+ ],
+ ]],
+ ],
+ ]],
+ ],
+ ]]);
+
+ $this->content->expects('getCommentsLink')
+ ->once()
+ ->andReturns('https://example.org/');
+ $this->settings->expects('getProcessor')
+ ->once()
+ ->andReturns('test');
+
+ $this->transformer->setClient($client);
+ $html = $this->transformer->transform($this->content);
+
+ $this->assertHtmlHasGeneratedContentContainer($html);
+ $this->assertHtmlHasMp4VideoWithAudio($html, 'https://example.org/DASH_video.mp4', 'https://example.org/DASH_audio.mp4');
+ }
+}