File: /home/xedaptot/ai.naniguide.com/tests/Unit/PersonalizationTagCatalogTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Services\PersonalizationTagCatalog;
use App\Model\Campaign;
use App\Model\MailList;
use App\Library\GeneralTagResolver;
use Illuminate\Support\Collection;
class PersonalizationTagCatalogTest extends TestCase
{
private function catalog(): PersonalizationTagCatalog
{
return app(PersonalizationTagCatalog::class);
}
public function test_campaign_tags_without_list_returns_campaign_and_general_only()
{
$items = $this->catalog()->campaignTags();
$keys = array_column($items, 'key');
foreach (['campaign.name', 'campaign.uid', 'campaign.from_email', 'campaign.from_name', 'campaign.subject', 'campaign.reply_to'] as $required) {
$this->assertContains($required, $keys, "Missing campaign key: {$required}");
}
foreach (['current_year', 'current_date', 'current_datetime'] as $required) {
$this->assertContains($required, $keys, "Missing general key: {$required}");
}
$this->assertNotContains('subscriber.email', $keys, 'Without list, subscriber.* must not appear');
$this->assertNotContains('list.name', $keys, 'Without list, list.* must not appear');
$this->assertNotContains('unsubscribe_url', $keys, 'Without list, advanced URLs must not appear');
}
public function test_items_have_required_shape()
{
$items = $this->catalog()->campaignTags();
$this->assertNotEmpty($items);
foreach ($items as $item) {
$this->assertArrayHasKey('key', $item);
$this->assertArrayHasKey('label', $item);
$this->assertArrayHasKey('description', $item);
$this->assertArrayHasKey('snippet', $item);
$this->assertArrayHasKey('group', $item);
$this->assertSame('{{' . $item['key'] . '}}', $item['snippet']);
$this->assertNotSame('', $item['label']);
}
}
public function test_with_list_adds_list_subscriber_and_advanced_scopes()
{
$list = $this->buildFakeList(customFields: []);
$items = $this->catalog()->campaignTags($list);
$keys = array_column($items, 'key');
foreach (['list.name', 'list.uid', 'list.from_email', 'list.from_name'] as $required) {
$this->assertContains($required, $keys, "Missing list key: {$required}");
}
foreach (['subscriber.uid', 'subscriber.email', 'subscriber.name', 'subscriber.full_name', 'subscriber.fullname'] as $required) {
$this->assertContains($required, $keys, "Missing subscriber key: {$required}");
}
foreach (['email', 'name', 'full_name', 'first_name', 'last_name'] as $required) {
$this->assertContains($required, $keys, "Missing global alias: {$required}");
}
foreach (['unsubscribe_url', 'update_profile_url', 'web_view_url'] as $required) {
$this->assertContains($required, $keys, "Missing advanced URL: {$required}");
}
}
public function test_custom_fields_are_included_except_core_tags()
{
$list = $this->buildFakeList(customFields: [
['tag' => 'FIRST_NAME', 'label' => 'First Name'], // core, should be skipped
['tag' => 'EMAIL', 'label' => 'Email'], // core, should be skipped
['tag' => 'COMPANY', 'label' => 'Company'],
['tag' => 'BIRTHDAY', 'label' => 'Birthday'],
]);
$items = $this->catalog()->campaignTags($list);
$keys = array_column($items, 'key');
$this->assertContains('subscriber.company', $keys);
$this->assertContains('subscriber.birthday', $keys);
$firstNameOccurrences = count(array_filter($keys, static fn ($k) => $k === 'subscriber.first_name'));
$this->assertSame(1, $firstNameOccurrences, 'Core tag FIRST_NAME must not be duplicated from custom fields');
}
public function test_snippets_use_double_curly_braces_without_spaces()
{
$items = $this->catalog()->campaignTags($this->buildFakeList());
foreach ($items as $item) {
$this->assertStringStartsWith('{{', $item['snippet']);
$this->assertStringEndsWith('}}', $item['snippet']);
}
}
public function test_general_keys_match_general_tag_resolver()
{
$items = $this->catalog()->campaignTags();
$catalogKeys = array_column($items, 'key');
$resolverKeys = array_keys((new GeneralTagResolver())->getResolvedTags());
foreach ($resolverKeys as $k) {
$this->assertContains($k, $catalogKeys, "GeneralTagResolver exposes '{$k}' but catalog does not");
}
}
public function test_campaign_keys_match_campaign_model_getresolvedtags()
{
$campaign = new Campaign([
'name' => 'Demo',
'from_email' => '[email protected]',
'from_name' => 'Demo Sender',
'subject' => 'Hello',
'reply_to' => '[email protected]',
]);
$campaign->uid = 'CUID';
$resolverKeys = $this->flattenLeafKeys($campaign->getResolvedTags());
$catalogKeys = array_column($this->catalog()->campaignTags(), 'key');
foreach ($resolverKeys as $k) {
$this->assertContains($k, $catalogKeys, "Campaign::getResolvedTags exposes '{$k}' but catalog does not");
}
}
public function test_list_keys_match_maillist_getresolvedtags()
{
$list = new MailList([
'name' => 'Weekly',
'from_email' => '[email protected]',
'from_name' => 'List Sender',
]);
$list->uid = 'LUID';
// Avoid DB hit on fields relation
$list->setRelation('fields', new Collection());
$resolverKeys = $this->flattenLeafKeys($list->getResolvedTags());
$catalogKeys = array_column($this->catalog()->campaignTags($list), 'key');
foreach ($resolverKeys as $k) {
$this->assertContains($k, $catalogKeys, "MailList::getResolvedTags exposes '{$k}' but catalog does not");
}
}
/**
* Build an in-memory MailList without touching the DB.
*/
private function buildFakeList(array $customFields = []): MailList
{
$list = new MailList([
'name' => 'Fake List',
'from_email' => '[email protected]',
'from_name' => 'Fake Sender',
]);
$list->uid = 'LIST-FAKE';
$fields = collect($customFields)->map(function ($data) {
$field = new \App\Model\Field([
'tag' => $data['tag'] ?? '',
'label' => $data['label'] ?? '',
'type' => $data['type'] ?? 'text',
]);
return $field;
});
$list->setRelation('fields', $fields);
return $list;
}
/**
* Flatten nested resolver output into dot-keys.
*
* @param array<string, mixed> $tags
* @return array<int, string>
*/
private function flattenLeafKeys(array $tags, string $prefix = ''): array
{
$out = [];
foreach ($tags as $k => $v) {
$full = $prefix !== '' ? $prefix . '.' . $k : (string) $k;
if (is_array($v)) {
$out = array_merge($out, $this->flattenLeafKeys($v, $full));
} else {
$out[] = $full;
}
}
return $out;
}
}