test: add PHPUnit test suite with JsonLdBuilder unit tests

16 unit tests covering FAQ, HowTo, Event, Recipe, LocalBusiness,
and VideoObject schema builders plus toScriptTag XSS escaping.
Closes #75
This commit is contained in:
Jonathan Miller
2026-06-23 12:31:18 -05:00
parent e7b0af1fca
commit cbebaecc22
5 changed files with 304 additions and 2 deletions
+1
View File
@@ -156,6 +156,7 @@ vendor/
composer.lock
*.phar
codeception.phar
.phpunit.cache/
.phpunit.result.cache
.php_cs.cache
.php-cs-fixer.cache
+16 -2
View File
@@ -15,9 +15,23 @@
"php": ">=8.1"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"joomla/coding-standards": "^3.0",
"phpstan/phpstan": "^1.10",
"joomla/coding-standards": "^3.0"
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.7"
},
"autoload": {
"psr-4": {
"Joomla\\Plugin\\System\\MokoOG\\": "source/packages/plg_system_mokoog/src/",
"Joomla\\Plugin\\Content\\MokoOG\\": "source/packages/plg_content_mokoog/src/",
"Joomla\\Plugin\\WebServices\\MokoOG\\": "source/packages/plg_webservices_mokoog/src/",
"Joomla\\Component\\MokoOG\\Administrator\\": "source/packages/com_mokoog/src/"
}
},
"autoload-dev": {
"psr-4": {
"Mokoconsulting\\MokoOG\\Tests\\": "tests/"
}
},
"minimum-stability": "alpha",
"prefer-stable": true,
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>source/packages</directory>
</include>
</source>
</phpunit>
+257
View File
@@ -0,0 +1,257 @@
<?php
/**
* @package MokoSuiteOpenGraph
* @subpackage Tests
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Mokoconsulting\MokoOG\Tests\Unit\Helper;
use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder;
use PHPUnit\Framework\TestCase;
class JsonLdBuilderTest extends TestCase
{
// ── FAQPage ──────────────────────────────────────────────────────
public function testBuildFaqReturnsNullForEmptyArray(): void
{
$this->assertNull(JsonLdBuilder::buildFaq([]));
}
public function testBuildFaqSkipsEmptyQuestions(): void
{
$faqs = [
['question' => '', 'answer' => 'An answer'],
['question' => 'Valid?', 'answer' => ''],
['question' => ' ', 'answer' => 'Still empty'],
];
$this->assertNull(JsonLdBuilder::buildFaq($faqs));
}
public function testBuildFaqReturnsValidSchema(): void
{
$faqs = [
['question' => 'What is OG?', 'answer' => 'Open Graph protocol.'],
['question' => 'Why use it?', 'answer' => 'Better social previews.'],
];
$result = JsonLdBuilder::buildFaq($faqs);
$this->assertNotNull($result);
$this->assertSame('https://schema.org', $result['@context']);
$this->assertSame('FAQPage', $result['@type']);
$this->assertCount(2, $result['mainEntity']);
$this->assertSame('Question', $result['mainEntity'][0]['@type']);
$this->assertSame('What is OG?', $result['mainEntity'][0]['name']);
$this->assertSame('Open Graph protocol.', $result['mainEntity'][0]['acceptedAnswer']['text']);
}
// ── HowTo ────────────────────────────────────────────────────────
public function testBuildHowToReturnsNullForEmptySteps(): void
{
$this->assertNull(JsonLdBuilder::buildHowTo('Test', []));
$this->assertNull(JsonLdBuilder::buildHowTo('Test', ['', ' ']));
}
public function testBuildHowToReturnsValidSchema(): void
{
$result = JsonLdBuilder::buildHowTo('Install Joomla', ['Download ZIP', 'Upload files', 'Run installer']);
$this->assertNotNull($result);
$this->assertSame('HowTo', $result['@type']);
$this->assertSame('Install Joomla', $result['name']);
$this->assertCount(3, $result['step']);
$this->assertSame(1, $result['step'][0]['position']);
$this->assertSame('HowToStep', $result['step'][0]['@type']);
$this->assertSame('Download ZIP', $result['step'][0]['text']);
$this->assertArrayNotHasKey('image', $result);
}
public function testBuildHowToIncludesImageWhenProvided(): void
{
$result = JsonLdBuilder::buildHowTo('Fix a bike', ['Remove wheel'], 'https://example.com/bike.jpg');
$this->assertNotNull($result);
$this->assertSame('https://example.com/bike.jpg', $result['image']);
}
// ── Recipe ───────────────────────────────────────────────────────
public function testBuildRecipeReturnsNullWhenNoData(): void
{
$data = (object) ['name' => '', 'description' => ''];
$this->assertNull(JsonLdBuilder::buildRecipe($data));
}
public function testBuildRecipeCalculatesTotalTime(): void
{
$data = (object) [
'name' => 'Pasta',
'prepTime' => 'PT15M',
'cookTime' => 'PT30M',
];
$result = JsonLdBuilder::buildRecipe($data);
$this->assertNotNull($result);
$this->assertSame('Recipe', $result['@type']);
$this->assertSame('PT45M', $result['totalTime']);
}
public function testBuildRecipeSplitsIngredientsByNewline(): void
{
$data = (object) [
'name' => 'Salad',
'ingredients' => "Lettuce\nTomato\nOnion",
];
$result = JsonLdBuilder::buildRecipe($data);
$this->assertNotNull($result);
$this->assertSame(['Lettuce', 'Tomato', 'Onion'], $result['recipeIngredient']);
}
// ── Event ────────────────────────────────────────────────────────
public function testBuildEventReturnsNullWithoutStartDate(): void
{
$data = (object) ['name' => 'Conference', 'startDate' => ''];
$this->assertNull(JsonLdBuilder::buildEvent($data));
}
public function testBuildEventIncludesLocationAndOffers(): void
{
$data = (object) [
'name' => 'Tech Summit',
'startDate' => '2026-09-01T09:00:00',
'endDate' => '2026-09-01T17:00:00',
'location' => (object) [
'name' => 'Convention Center',
'address' => '123 Main St',
],
'offers' => (object) [
'price' => '99.00',
'currency' => 'EUR',
'url' => 'https://example.com/tickets',
],
];
$result = JsonLdBuilder::buildEvent($data);
$this->assertNotNull($result);
$this->assertSame('Event', $result['@type']);
$this->assertSame('2026-09-01T09:00:00', $result['startDate']);
$this->assertSame('2026-09-01T17:00:00', $result['endDate']);
$this->assertSame('Place', $result['location']['@type']);
$this->assertSame('Convention Center', $result['location']['name']);
$this->assertSame('Offer', $result['offers']['@type']);
$this->assertSame('99.00', $result['offers']['price']);
$this->assertSame('EUR', $result['offers']['priceCurrency']);
}
// ── LocalBusiness ────────────────────────────────────────────────
public function testBuildLocalBusinessReturnsNullWithoutName(): void
{
$params = $this->createParamsMock([]);
$this->assertNull(JsonLdBuilder::buildLocalBusiness($params));
}
public function testBuildLocalBusinessIncludesAddress(): void
{
$params = $this->createParamsMock([
'business_name' => 'Moko Consulting',
'street_address' => '456 Oak Ave',
'city' => 'Austin',
'region' => 'TX',
'postal_code' => '78701',
'country' => 'US',
'telephone' => '+1-555-0100',
]);
$result = JsonLdBuilder::buildLocalBusiness($params);
$this->assertNotNull($result);
$this->assertSame('LocalBusiness', $result['@type']);
$this->assertSame('Moko Consulting', $result['name']);
$this->assertSame('PostalAddress', $result['address']['@type']);
$this->assertSame('456 Oak Ave', $result['address']['streetAddress']);
$this->assertSame('Austin', $result['address']['addressLocality']);
$this->assertSame('TX', $result['address']['addressRegion']);
$this->assertSame('78701', $result['address']['postalCode']);
$this->assertSame('US', $result['address']['addressCountry']);
$this->assertSame('+1-555-0100', $result['telephone']);
}
// ── VideoObject ──────────────────────────────────────────────────
public function testBuildVideoReturnsNullForEmptyUrl(): void
{
$this->assertNull(JsonLdBuilder::buildVideo(''));
}
public function testBuildVideoAddsEmbedUrlForYoutube(): void
{
$result = JsonLdBuilder::buildVideo(
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'Test Video',
'A description'
);
$this->assertNotNull($result);
$this->assertSame('VideoObject', $result['@type']);
$this->assertSame('https://www.youtube.com/watch?v=dQw4w9WgXcQ', $result['contentUrl']);
$this->assertSame('https://www.youtube.com/embed/dQw4w9WgXcQ', $result['embedUrl']);
$this->assertSame('Test Video', $result['name']);
}
// ── toScriptTag ──────────────────────────────────────────────────
public function testToScriptTagEscapesClosingScriptTags(): void
{
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => 'Test </script><script>alert(1)</script>',
];
$output = JsonLdBuilder::toScriptTag($schema);
$this->assertStringStartsWith('<script type="application/ld+json">', $output);
$this->assertStringEndsWith('</script>', $output);
// The closing </script> inside the JSON must be escaped
$this->assertStringNotContainsString('</script><script>', $output);
$this->assertStringContainsString('<\\/script>', $output);
}
// ── Helper ───────────────────────────────────────────────────────
/**
* Create a mock object that mimics Joomla's Registry->get($key, $default).
*/
private function createParamsMock(array $values): object
{
return new class ($values) {
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function get(string $key, mixed $default = ''): mixed
{
return $this->data[$key] ?? $default;
}
};
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
/**
* @package MokoSuiteOpenGraph
* @subpackage Tests
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
define('_JEXEC', 1);
require_once __DIR__ . '/../vendor/autoload.php';