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/Feature/AdsR15_7CatalogUiTest.php
<?php

/**
 * Ads Phase R15.7 — Catalog UI polish (backend bits).
 *
 * Pest tests cover the controller / service / form-request changes; pure
 * visual styling is left to manual / screenshot audit.
 *
 * Verifies:
 *   1. Create form passes $platforms (active only, current customer) to
 *      the view.
 *   2. StoreRequest accepts ad_platform_id + consumer_key + consumer_secret
 *      + currency; rejects when platform belongs to another customer.
 *   3. StoreRequest requires consumer_key + consumer_secret when
 *      source_type=woocommerce (required_if rule).
 *   4. Service::create composes consumer_key + consumer_secret + currency
 *      into encrypted source_credentials JSON.
 *   5. Service::create persists ad_platform_id binding.
 *   6. Errors endpoint returns the catalog's error_log + name.
 *   7. Errors endpoint enforces tenant scope (404 when uid belongs to
 *      another customer).
 *   8. Listing partial renders the new platform / errors columns.
 *   9. Listing partial flags rows with sync_status=in_progress for the
 *      poller via data-in-progress-count.
 *  10. Errors-count badge renders a clickable button when error_log
 *      has rows.
 */

use App\Model\AdPlatform;
use App\Model\AdProductCatalog;
use App\Model\Customer;
use App\Services\Ads\AdProductCatalogService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;

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

beforeEach(function () {
    // Per the SaaS-subscription middleware memory: tests must use a
    // DemoSeeder-seeded customer (factory() users have no subscription and
    // get redirected by middleware).  Pick the first available customer
    // with a user attached.
    $this->customer = Customer::whereHas('user')->orderBy('id')->first();
    if (! $this->customer) {
        $this->markTestSkipped('No seeded customer with user — run DemoSeeder first');
    }
    $this->user = $this->customer->user;
    $this->customerId = $this->customer->local()->id;
});

function r157Platform(int $customerId, string $platformKey = 'meta', string $name = 'R15.7 Test Platform', string $status = AdPlatform::STATUS_ACTIVE): AdPlatform
{
    $platform = new AdPlatform([
        'uid' => 'r157plat_' . uniqid(),
        'customer_id' => $customerId,
        'platform' => $platformKey,
        'name' => $name,
        'access_token' => 'r157tok_' . uniqid(),
        'platform_user_id' => 'p_' . uniqid(),
        'platform_user_name' => 'r157user',
    ]);
    $platform->status = $status;
    $platform->health_status = AdPlatform::HEALTH_HEALTHY;
    $platform->save();
    return $platform;
}

// ---------------------------------------------------------------------------
// 1. Create form passes $platforms (active only)
// ---------------------------------------------------------------------------

test('create() passes active customer platforms to the view, excludes inactive', function () {
    r157Platform($this->customerId, 'meta', 'R15.7 Active Meta');
    r157Platform($this->customerId, 'google', 'R15.7 Stale Google', AdPlatform::STATUS_REVOKED);

    $response = $this->actingAs($this->user)->get(route('refactor.ads.catalogs.create'));

    $response->assertOk();
    $response->assertSee('R15.7 Active Meta');
    $response->assertDontSee('R15.7 Stale Google');
});

// ---------------------------------------------------------------------------
// 2-3. StoreRequest validation
// ---------------------------------------------------------------------------

test('StoreRequest rejects ad_platform_id belonging to another customer (tenant scope)', function () {
    // Build a second customer + their own platform.
    $otherCustomer = Customer::whereHas('user')->where('id', '!=', $this->customer->id)->first();
    if (! $otherCustomer) {
        $this->markTestSkipped('Need at least 2 seeded customers');
    }
    $otherPlatform = r157Platform($otherCustomer->local()->id, 'tiktok', 'Other Customer TikTok');

    $response = $this->actingAs($this->user)->postJson(route('refactor.ads.catalogs.store'), [
        'name' => 'Cross-tenant attack',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'ad_platform_id' => $otherPlatform->id,
    ]);

    $response->assertStatus(422);
    $response->assertJsonValidationErrors(['ad_platform_id']);
});

test('StoreRequest requires consumer_key + consumer_secret when source_type=woocommerce', function () {
    $response = $this->actingAs($this->user)->postJson(route('refactor.ads.catalogs.store'), [
        'name' => 'WC catalog R15.7',
        'source_type' => AdProductCatalog::SOURCE_WOOCOMMERCE,
        'source_url' => 'https://store.example.test',
    ]);

    $response->assertStatus(422);
    $response->assertJsonValidationErrors(['consumer_key', 'consumer_secret']);
});

test('StoreRequest passes when source_type=manual with no creds', function () {
    $response = $this->actingAs($this->user)->postJson(route('refactor.ads.catalogs.store'), [
        'name' => 'Manual catalog R15.7',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
    ]);

    $response->assertOk();
    $response->assertJsonPath('status', 'success');
});

// ---------------------------------------------------------------------------
// 4-5. Service::create composes encrypted source_credentials + binds platform
// ---------------------------------------------------------------------------

test('Service::create composes consumer_key/consumer_secret/currency into encrypted source_credentials', function () {
    $service = app(AdProductCatalogService::class);
    $catalog = $service->create($this->customer->local(), [
        'name' => 'WC encrypted',
        'source_type' => AdProductCatalog::SOURCE_WOOCOMMERCE,
        'source_url' => 'https://store.example.test',
        'consumer_key' => 'ck_secret_value',
        'consumer_secret' => 'cs_more_secret',
        'currency' => 'EUR',
    ]);

    expect($catalog->source_credentials)->toBe([
        'consumer_key' => 'ck_secret_value',
        'consumer_secret' => 'cs_more_secret',
        'currency' => 'EUR',
    ]);

    $raw = (string) DB::table('ad_product_catalogs')->where('id', $catalog->id)->value('source_credentials');
    expect($raw)->not->toContain('ck_secret_value');
    expect($raw)->not->toContain('cs_more_secret');
});

test('Service::create persists ad_platform_id binding', function () {
    $platform = r157Platform($this->customerId, 'meta', 'Bind target');
    $service = app(AdProductCatalogService::class);

    $catalog = $service->create($this->customer->local(), [
        'name' => 'Bound catalog',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'ad_platform_id' => $platform->id,
    ]);

    expect($catalog->ad_platform_id)->toBe($platform->id);
});

test('Service::create with empty ad_platform_id leaves binding null', function () {
    $service = app(AdProductCatalogService::class);
    $catalog = $service->create($this->customer->local(), [
        'name' => 'Unbound',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'ad_platform_id' => '',
    ]);

    expect($catalog->ad_platform_id)->toBeNull();
});

// ---------------------------------------------------------------------------
// 6-7. Errors endpoint
// ---------------------------------------------------------------------------

test('errors endpoint returns the catalog name + error_log', function () {
    $catalog = AdProductCatalog::create([
        'uid' => 'r157err_' . uniqid(),
        'customer_id' => $this->customerId,
        'name' => 'Faulty Catalog R15.7',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'sync_status' => AdProductCatalog::SYNC_PARTIAL,
        'error_log' => [
            ['stage' => 'sync', 'external_id' => 'sku-A', 'error' => 'invalid image'],
            ['stage' => 'source_pull', 'external_id' => 'sku-B', 'error' => 'http_404'],
        ],
    ]);

    $response = $this->actingAs($this->user)->getJson(route('refactor.ads.catalogs.errors', $catalog->uid));

    $response->assertOk();
    $response->assertJsonPath('status', 'success');
    $response->assertJsonPath('name', 'Faulty Catalog R15.7');
    $response->assertJsonCount(2, 'errors');
    $response->assertJsonPath('errors.0.external_id', 'sku-A');
    $response->assertJsonPath('errors.1.stage', 'source_pull');
});

test('errors endpoint 404 when catalog belongs to another customer', function () {
    $otherCustomer = Customer::whereHas('user')->where('id', '!=', $this->customer->id)->first();
    if (! $otherCustomer) {
        $this->markTestSkipped('Need at least 2 seeded customers');
    }

    $otherCatalog = AdProductCatalog::create([
        'uid' => 'r157oth_' . uniqid(),
        'customer_id' => $otherCustomer->local()->id,
        'name' => 'Cross-tenant catalog',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'sync_status' => AdProductCatalog::SYNC_PENDING,
    ]);

    $response = $this->actingAs($this->user)->getJson(route('refactor.ads.catalogs.errors', $otherCatalog->uid));
    $response->assertStatus(404);
});

// ---------------------------------------------------------------------------
// 8-10. Listing partial markup
// ---------------------------------------------------------------------------

test('listing partial renders new platform + errors columns', function () {
    $platform = r157Platform($this->customerId, 'meta', 'Listing Meta');
    AdProductCatalog::create([
        'uid' => 'r157lst_' . uniqid(),
        'customer_id' => $this->customerId,
        'ad_platform_id' => $platform->id,
        'name' => 'Listing Test Catalog R15.7',
        'source_type' => AdProductCatalog::SOURCE_WOOCOMMERCE,
        'sync_status' => AdProductCatalog::SYNC_COMPLETED,
    ]);

    $response = $this->actingAs($this->user)->get(route('refactor.ads.catalogs.listing'));

    $response->assertOk();
    $response->assertSee('Platform');
    $response->assertSee('Errors');
    $response->assertSee('Meta');
});

test('listing partial flags table with data-in-progress-count for the poller', function () {
    AdProductCatalog::create([
        'uid' => 'r157prog_' . uniqid(),
        'customer_id' => $this->customerId,
        'name' => 'Mid-flight R15.7',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'sync_status' => AdProductCatalog::SYNC_IN_PROGRESS,
    ]);

    $response = $this->actingAs($this->user)->get(route('refactor.ads.catalogs.listing'));

    $response->assertOk();
    $response->assertSee('data-in-progress-count', false);
    $response->assertSee('mc-catalog-syncing', false);
});

test('listing partial renders error count as a clickable badge button', function () {
    AdProductCatalog::create([
        'uid' => 'r157eb_' . uniqid(),
        'customer_id' => $this->customerId,
        'name' => 'Badge Catalog R15.7',
        'source_type' => AdProductCatalog::SOURCE_MANUAL,
        'sync_status' => AdProductCatalog::SYNC_PARTIAL,
        'error_log' => [
            ['stage' => 'sync', 'external_id' => 'sku-X', 'error' => 'whatever'],
            ['stage' => 'sync', 'external_id' => 'sku-Y', 'error' => 'whatever'],
            ['stage' => 'source_pull', 'external_id' => 'sku-Z', 'error' => 'whatever'],
        ],
    ]);

    $response = $this->actingAs($this->user)->get(route('refactor.ads.catalogs.listing'));

    $response->assertOk();
    $response->assertSee('data-action="show-catalog-errors"', false);
    $response->assertSee('mc-badge-clickable', false);
});