cbebaecc22
16 unit tests covering FAQ, HowTo, Event, Recipe, LocalBusiness, and VideoObject schema builders plus toScriptTag XSS escaping. Closes #75
258 lines
9.9 KiB
PHP
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;
|
|
}
|
|
};
|
|
}
|
|
}
|