HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/tests/Unit/SubscriberRowNormalizerTest.php
<?php

/**
 * SubscriberRowNormalizerTest — pure-PHP tests for the JSON → canonical row
 * translation. Hits a real DB because the normalizer reads the list's fields.
 *
 * Surface covered:
 *   - Case-insensitive tag matching (EMAIL / Email / email)
 *   - Email lowercase + whitespace strip
 *   - Status validation + fallback
 *   - Tags accept array OR comma-string
 *   - Unknown field becomes a soft error (row not rejected outright)
 *   - Missing EMAIL → row rejected
 *   - Invalid email format → row rejected
 */

use App\Library\Bulk\SubscriberRowNormalizer;
use App\Model\Customer;
use App\Model\MailList;
use App\Model\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;

uses(TestCase::class);
uses(DatabaseTransactions::class);

beforeEach(function () {
    $this->customer = Customer::forceCreate([
        'uid' => uniqid('norm_c_'), 'timezone' => 'UTC', 'status' => 'active',
    ]);
    $this->user = User::forceCreate([
        'customer_id' => $this->customer->id, 'uid' => uniqid('norm_u_'),
        'email' => uniqid('u').'@norm.local', 'password' => bcrypt('x'),
    ]);
    $this->list = MailList::forceCreate([
        'uid' => uniqid('norm_l_'), 'customer_id' => $this->customer->id,
        'name' => 'NormTest', 'from_email' => '[email protected]', 'from_name' => 'N',
        'send_welcome_email' => false, 'subscribe_confirmation' => false,
        'unsubscribe_notification' => false,
    ]);

    $this->emailCol     = $this->list->getEmailField()->custom_field_name;
    $this->firstNameCol = $this->list->fields()->where('tag', 'FIRST_NAME')->first()->custom_field_name;
});

test('case-insensitive tag matching: EMAIL / Email / email all map to email column', function () {
    foreach (['EMAIL', 'Email', 'email'] as $key) {
        [$row, $errors] = SubscriberRowNormalizer::normalize([$key => '[email protected]'], $this->list);
        expect($errors)->toBe([]);
        expect($row[$this->emailCol])->toBe('[email protected]');
    }
});

test('email lowercased + whitespace + BOM stripped', function () {
    [$row, $errors] = SubscriberRowNormalizer::normalize([
        'EMAIL' => "  [email protected]  ",
        'FIRST_NAME' => 'Alice',
    ], $this->list);
    expect($errors)->toBe([]);
    expect($row[$this->emailCol])->toBe('[email protected]');
    expect($row[$this->firstNameCol])->toBe('Alice');
});

test('missing EMAIL → row rejected with errors', function () {
    [$row, $errors] = SubscriberRowNormalizer::normalize(['FIRST_NAME' => 'X'], $this->list);
    expect($row)->toBeNull();
    expect($errors)->toHaveKey('EMAIL');
});

test('invalid email format → row rejected', function () {
    [$row, $errors] = SubscriberRowNormalizer::normalize(['EMAIL' => 'not-an-email'], $this->list);
    expect($row)->toBeNull();
    expect($errors['EMAIL'])->toContain('invalid email format');
});

test('unknown JSON field → soft error, row still kept', function () {
    [$row, $errors] = SubscriberRowNormalizer::normalize([
        'EMAIL' => '[email protected]',
        'BIRTHDAY' => '1990-01-01',     // no BIRTHDAY field on this list
    ], $this->list);
    expect($row)->not->toBeNull();
    expect($row[$this->emailCol])->toBe('[email protected]');
    expect($errors)->toHaveKey('BIRTHDAY');
    expect($errors['BIRTHDAY'])->toContain('unknown field');
});

test('status passes through allow-list, invalid falls back to subscribed', function () {
    [$row1, ] = SubscriberRowNormalizer::normalize(['EMAIL' => '[email protected]', 'status' => 'unsubscribed'], $this->list);
    expect($row1['status'])->toBe('unsubscribed');

    [$row2, ] = SubscriberRowNormalizer::normalize(['EMAIL' => '[email protected]', 'status' => 'bogus'], $this->list);
    expect($row2['status'])->toBe('subscribed');

    // Missing status → default subscribed (for INSERT path)
    [$row3, ] = SubscriberRowNormalizer::normalize(['EMAIL' => '[email protected]'], $this->list);
    expect($row3['status'])->toBe('subscribed');
});

test('tags accept array → JSON-encoded', function () {
    [$row, ] = SubscriberRowNormalizer::normalize([
        'EMAIL' => '[email protected]',
        'tags' => ['vip', 'newsletter'],
    ], $this->list);
    expect(json_decode($row['tags'], true))->toBe(['vip', 'newsletter']);
});

test('tags accept comma-string → JSON-encoded array', function () {
    [$row, ] = SubscriberRowNormalizer::normalize([
        'EMAIL' => '[email protected]',
        'tags' => 'vip, newsletter, beta',
    ], $this->list);
    expect(json_decode($row['tags'], true))->toBe(['vip', 'newsletter', 'beta']);
});

test('empty tags array or null → tags stored as null', function () {
    [$row1, ] = SubscriberRowNormalizer::normalize(['EMAIL' => '[email protected]', 'tags' => []], $this->list);
    expect($row1['tags'])->toBeNull();

    [$row2, ] = SubscriberRowNormalizer::normalize(['EMAIL' => '[email protected]', 'tags' => null], $this->list);
    expect($row2['tags'])->toBeNull();
});

test('tag matching uses TAG not LABEL — "First Name" label ≠ tag', function () {
    // The list's FIRST_NAME field has label "First name" (or similar). The
    // contract says only TAG matches — sending label should be unknown_field.
    [$row, $errors] = SubscriberRowNormalizer::normalize([
        'EMAIL' => '[email protected]',
        'First name' => 'Alice',         // label, not tag
    ], $this->list);
    expect($errors)->toHaveKey('First name');
    expect($row[$this->firstNameCol] ?? null)->toBeNull();
});