Files
MokoSuiteOpenGraph/tests/Unit/Helper/JsonLdBuilderTest.php
T
Jonathan Miller cbebaecc22 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
2026-06-23 12:54:00 -05:00

258 lines
9.9 KiB
PHP

<?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;
}
};
}
}