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/ai.naniguide.com/tests/Feature/SendingIdentityVisibilityTest.php
<?php

/**
 * SendingIdentityVisibilityTest — pins the design-doc § 9 contract on
 * `IdentityCatalog::valuesForCustomer` (and `SendingIdentity::scopeVisibleTo`)
 * for the "user does NOT own a sending server" case — i.e. the plan flows
 * the customer onto a system-attached SendingServer.
 *
 * Past incident 2026-05-20 (Abu Sadeq / marketing.zartech.net): the admin
 * Sender Identity tab labeled its checkbox "Available for all" but the save
 * path wrote `enabled` instead of `shared`. Customer Free plan → SendGrid
 * primary server → 5 admin-managed identities sitting at `enabled=true,
 * shared=false` → customer campaign From: dropdown empty. This test catches
 * that class of regression by asserting the contract from the customer's
 * side regardless of what the admin UI does.
 */

use App\Model\Customer;
use App\Model\Plan;
use App\Model\PlanEntitlement;
use App\Model\PlansSendingServer;
use App\Model\SendingIdentity;
use App\Model\SendingServer;
use App\Model\Subscription;
use App\SendingServers\Identities\IdentityCatalog;
use App\SendingServers\Identities\IdentityKind;
use App\SendingServers\Identities\IdentityStatus;
use App\SendingServers\Identities\ManagementMode;
use App\Services\Plans\Entitlements\EntitlementKey;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

uses(TestCase::class);

beforeEach(function () {
    // Customer that uses the system-attached server (NOT their own).
    $this->customer = Customer::forceCreate([
        'uid'      => uniqid('vis_c_'),
        'timezone' => 'UTC',
        'status'   => 'active',
    ]);

    // Plan: explicit HAS_OWN_SENDING_SERVER = false (system server flow).
    $this->plan = Plan::forceCreate([
        'uid'              => uniqid('vis_plan_'),
        'name'             => 'System Server Plan',
        'currency_id'      => 1,
        'frequency_amount' => 1,
        'frequency_unit'   => 'month',
        'price'            => 0,
        'status'           => 'active',
    ]);
    PlanEntitlement::factory()->for($this->plan, 'plan')
        ->forKey(EntitlementKey::HAS_OWN_SENDING_SERVER)
        ->create(); // enabled defaults to false → plan uses system server

    // Admin-owned SendingServer (customer_id = NULL) attached to the plan
    // as primary — this is the server whose identities the customer must
    // be able to pick from.
    $this->server = SendingServer::factory()->create([
        'admin_id'    => 1,
        'customer_id' => null,
        'type'        => 'smtp', // generic; cross-domain irrelevant for visibility
        'status'      => SendingServer::STATUS_ACTIVE,
    ]);
    PlansSendingServer::forceCreate([
        'plan_id'           => $this->plan->id,
        'sending_server_id' => $this->server->id,
        'is_primary'        => true,
        'fitness'           => 100,
    ]);

    // Active subscription binds customer → plan.
    $this->sub = Subscription::create([
        'uid'                    => (string) \Illuminate\Support\Str::uuid(),
        'customer_id'            => $this->customer->id,
        'plan_id'                => $this->plan->id,
        'status'                 => 'active',
        'current_period_ends_at' => now()->addMonth(),
    ]);

    $this->catalog = app(IdentityCatalog::class);
});

afterEach(function () {
    DB::table('sending_identities')->where('sending_server_id', $this->server->id)->delete();
    DB::table('plans_sending_servers')->where('plan_id', $this->plan->id)->delete();
    DB::table('plan_entitlements')->where('plan_id', $this->plan->id)->delete();
    $this->sub->delete();
    $this->server->delete();
    $this->plan->delete();
    $this->customer->delete();
});

// ─── core contract ──────────────────────────────────────────────────────────

test('shared=true admin-managed identity IS visible to customer on system server', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id,
        'customer_id'       => null,                          // admin-managed
        'kind'              => IdentityKind::EMAIL,
        'value'             => '[email protected]',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::VENDOR_SYNC,
        'shared'            => true,                          // <-- under test
        'enabled'           => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->toContain('[email protected]');
});

test('shared=false admin-managed identity is NOT visible to customer', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id,
        'customer_id'       => null,
        'kind'              => IdentityKind::EMAIL,
        'value'             => '[email protected]',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::VENDOR_SYNC,
        'shared'            => false,                         // <-- under test
        'enabled'           => true,                          // enabled flips don't help
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->not->toContain('[email protected]');
});

test('mixed batch: dropdown contains shared rows, hides unshared rows', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => false, 'enabled' => true,
    ]);
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->toContain('[email protected]');
    expect($values)->toContain('[email protected]');
    expect($values)->not->toContain('[email protected]');
});

test('enabled=false hides identity even when shared=true', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => false,                 // soft-paused
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->not->toContain('[email protected]');
});

test('archived (non-VERIFIED) identity is NOT visible even if shared=true', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::ARCHIVED,                  // not verified
        'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->not->toContain('[email protected]');
});

// ─── regression test for the zartech 2026-05-20 bug ──────────────────────────

test('regression 2026-05-20: legacy state enabled=true + shared=false → invisible', function () {
    // Reproduces the exact pre-fix state seen on marketing.zartech.net:
    // VendorSync inserted rows with shared=false default; admin tick of
    // "Available for all" wrote enabled=true (the wrong column) so shared
    // stayed false; visibility query returned 0 results.
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => false, 'enabled' => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    // Bug repro: must be EMPTY in this state. Post-backfill migration runs
    // shared = enabled for admin-managed rows; afterwards the row would
    // satisfy the visibility query.
    expect($values)->not->toContain('[email protected]');
});

// ─── bulkUpdateShared writes the correct column ──────────────────────────────

test('bulkUpdateShared flips shared on selected, false on the rest', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => false, 'enabled' => true,
    ]);
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,                  // currently shared
    ]);

    $this->server->bulkUpdateShared([
        'emails' => ['[email protected]'],
        'domains' => [],
    ]);

    $pick = SendingIdentity::where('value', '[email protected]')->first();
    $skip = SendingIdentity::where('value', '[email protected]')->first();

    expect($pick->shared)->toBeTrue();
    expect($skip->shared)->toBeFalse();           // was true, now false (not in selection)
    // `enabled` is left untouched by bulkUpdateShared (separate concern).
    expect($pick->enabled)->toBeTrue();
    expect($skip->enabled)->toBeTrue();
});

// ─── bulkUpdateShared edge cases ────────────────────────────────────────────

test('bulkUpdateShared with empty selection flips ALL to false', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);

    // Admin unchecks every box and saves → controller forwards empty arrays.
    $this->server->bulkUpdateShared(['emails' => [], 'domains' => []]);

    expect(SendingIdentity::where('value', '[email protected]')->first()->shared)->toBeFalse();
    expect(SendingIdentity::where('value', '[email protected]')->first()->shared)->toBeFalse();
});

test('bulkUpdateShared is scoped to THIS server, never touches other servers', function () {
    // Second admin-owned server in the same instance — a leakage of bulk
    // updates across servers would be a security/visibility regression.
    $otherServer = SendingServer::factory()->create([
        'admin_id' => 1, 'customer_id' => null,
        'type' => 'smtp', 'status' => SendingServer::STATUS_ACTIVE,
    ]);
    SendingIdentity::create([
        'sending_server_id' => $otherServer->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,                  // shared on the OTHER server
    ]);

    // Bulk-share an identity on $this->server; pass the OTHER server's
    // identity value in the payload (simulates a malicious / buggy POST).
    $this->server->bulkUpdateShared([
        'emails' => ['[email protected]'],
        'domains' => [],
    ]);

    // The other server's identity must NOT be touched — bulkUpdateShared
    // scopes its UPDATE to $this->identities() (this server only).
    $other = SendingIdentity::where('sending_server_id', $otherServer->id)
        ->where('value', '[email protected]')->first();
    expect($other->shared)->toBeTrue();                       // untouched

    DB::table('sending_identities')->where('sending_server_id', $otherServer->id)->delete();
    $otherServer->delete();
});

// ─── customer-owned identities (branches 1+2 of scopeVisibleTo) ─────────────

test('admin-server identity owned by customer X IS visible to X even if shared=false', function () {
    // Branch 3 of scopeVisibleTo: admin-server, shared=false BUT customer_id=X
    // means X registered this identity themselves (manual add or self-service
    // vendor-sync). Only X sees it; other customers don't ("Private").
    SendingIdentity::create([
        'sending_server_id' => $this->server->id,
        'customer_id'       => $this->customer->id,           // owned by THIS customer
        'kind'              => IdentityKind::EMAIL,
        'value'             => '[email protected]',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::VENDOR_SYNC,
        'shared'            => false,                          // NOT shared
        'enabled'           => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->toContain('[email protected]');
});

test('admin-server identity owned by customer Y is NOT visible to customer X', function () {
    // Same scenario but from a different customer's perspective.
    $other = Customer::forceCreate([
        'uid' => uniqid('vis_c_other_'), 'timezone' => 'UTC', 'status' => 'active',
    ]);
    SendingIdentity::create([
        'sending_server_id' => $this->server->id,
        'customer_id'       => $other->id,                     // owned by OTHER customer
        'kind'              => IdentityKind::EMAIL,
        'value'             => '[email protected]',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::VENDOR_SYNC,
        'shared'            => false,
        'enabled'           => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->not->toContain('[email protected]');

    $other->delete();
});

test('server-less LOCAL_DKIM identity is visible only to its owner', function () {
    // Branch 1 of scopeVisibleTo: sending_server_id IS NULL → owner-only.
    $other = Customer::forceCreate([
        'uid' => uniqid('vis_c_dkim_'), 'timezone' => 'UTC', 'status' => 'active',
    ]);

    // Customer-X-owned LOCAL_DKIM domain.
    SendingIdentity::create([
        'sending_server_id' => null,
        'customer_id'       => $this->customer->id,
        'kind'              => IdentityKind::DOMAIN,
        'value'             => 'my.example.com',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::LOCAL_DKIM,
        'shared'            => false,                          // shared is irrelevant for server-less
        'enabled'           => true,
        'dkim_private'      => 'TEST_PRIVATE_KEY',
        'dkim_public'       => 'TEST_PUBLIC_KEY',
    ]);

    // The plan's primary server is type=smtp which DOES implement
    // AllowsLocalDomainVerify, so the LOCAL_DKIM identity passes isAllowedBy.
    $own = $this->catalog->valuesForCustomer($this->customer);
    expect($own)->toContain('my.example.com');

    // Different customer must not see it.
    $foreign = $this->catalog->valuesForCustomer($other);
    expect($foreign)->not->toContain('my.example.com');

    $other->delete();
});

// ─── cross-server isAllowedBy gate (vendor-strict primary) ───────────────────

test('primary server without AllowsCrossSendingDomain hides identity tied to a different server', function () {
    // Promote the plan's primary to a vendor-strict driver (sendgrid-api
    // does NOT implement AllowsCrossSendingDomain). An identity tied to
    // the OLD primary (type=smtp) becomes ineligible — sendgrid-api would
    // reject the From: at send time, so the dropdown hides it upfront.
    $strict = SendingServer::factory()->create([
        'admin_id'    => 1,
        'customer_id' => null,
        'type'        => 'sendgrid-api',
        'config'      => ['sendgrid_api_key' => 'SG.fake'],
        'status'      => SendingServer::STATUS_ACTIVE,
    ]);
    // Re-point the plan's primary to the strict server.
    DB::table('plans_sending_servers')->where('plan_id', $this->plan->id)
        ->update(['sending_server_id' => $strict->id]);

    // Identity sits on the original smtp server (this->server), shared=true.
    SendingIdentity::create([
        'sending_server_id' => $this->server->id,             // smtp, NOT the primary
        'customer_id'       => null,
        'kind'              => IdentityKind::EMAIL,
        'value'             => '[email protected]',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::VENDOR_SYNC,
        'shared'            => true,
        'enabled'           => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    // Visibility scope passes (shared=true), but cross-server narrow
    // (isAllowedBy(sendgrid-api primary) returns false because sendgrid-api
    // doesn't implement AllowsCrossSendingDomain) filters it out.
    expect($values)->not->toContain('[email protected]');

    DB::table('sending_identities')->where('sending_server_id', $strict->id)->delete();
    $strict->delete();
});

test('identity tied to the SAME server as plan-primary is always allowed (isAllowedBy short-circuit)', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id,             // == plan primary
        'customer_id'       => null,
        'kind'              => IdentityKind::EMAIL,
        'value'             => '[email protected]',
        'status'            => IdentityStatus::VERIFIED,
        'management_mode'   => ManagementMode::VENDOR_SYNC,
        'shared'            => true,
        'enabled'           => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->toContain('[email protected]');
});

// ─── plan has NO primary server attached ────────────────────────────────────

test('plan without primary server: all visible identities surface (no isAllowedBy narrow)', function () {
    // Detach the primary so plan->primarySendingServer() returns null.
    // valuesForCustomer branch line 60-67: "No plan-bound system server →
    // expose everything visible. (Own-server flow handles its own routing
    // at send time.)"
    DB::table('plans_sending_servers')->where('plan_id', $this->plan->id)->delete();

    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);

    $values = $this->catalog->valuesForCustomer($this->customer);

    expect($values)->toContain('[email protected]');
});

// ─── droplistForCustomer formatting (autocomplete API shape) ────────────────

test('droplistForCustomer wraps an EMAIL identity in {text,value,desc} for the autocomplete API', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);

    $rows = $this->catalog->droplistForCustomer($this->customer, null);

    // Per IdentityCatalog::formatDroplist, EMAIL rows have a 'value' key
    // (DOMAIN rows have 'subfix' instead). The autocomplete JS reads
    // 'value' for the chosen From: address.
    $emailRow = collect($rows)->first(
        fn ($r) => is_array($r) && ($r['value'] ?? null) === '[email protected]'
    );
    expect($emailRow)->not->toBeNull();
    expect($emailRow)->toHaveKeys(['text', 'value', 'desc']);
});

// ─── Campaign::rules() backend enforcement of from_email ─────────────────────

test('Campaign::rules from_email accepts a verified shared identity', function () {
    SendingIdentity::create([
        'sending_server_id' => $this->server->id, 'customer_id' => null,
        'kind' => IdentityKind::EMAIL, 'value' => '[email protected]',
        'status' => IdentityStatus::VERIFIED, 'management_mode' => ManagementMode::VENDOR_SYNC,
        'shared' => true, 'enabled' => true,
    ]);

    $campaign = new \App\Model\Campaign();
    $campaign->customer_id = $this->customer->id;
    $campaign->use_default_sending_server_from_email = false;
    $campaign->setRelation('customer', $this->customer);

    $rules = $campaign->rules();
    $validator = \Illuminate\Support\Facades\Validator::make(
        ['from_email' => '[email protected]', 'name' => 'X', 'subject' => 'S',
         'from_name' => 'N', 'reply_to' => '[email protected]'],
        $rules
    );

    expect($validator->errors()->has('from_email'))->toBeFalse();
});

test('Campaign::rules from_email rejects an unverified address (backend gate)', function () {
    // No identity created — verified list is empty. Posting a from_email
    // must be rejected by the closure rule.
    $campaign = new \App\Model\Campaign();
    $campaign->customer_id = $this->customer->id;
    $campaign->use_default_sending_server_from_email = false;
    $campaign->setRelation('customer', $this->customer);

    $rules = $campaign->rules();
    $validator = \Illuminate\Support\Facades\Validator::make(
        ['from_email' => '[email protected]', 'name' => 'X', 'subject' => 'S',
         'from_name' => 'N', 'reply_to' => '[email protected]'],
        $rules
    );

    expect($validator->errors()->has('from_email'))->toBeTrue();
});

test('Campaign::rules from_email skips closure when allowUnverifiedFromEmailAddress=true', function () {
    // Flip the SendingServer's option so allowUnverifiedFromEmailAddress() returns true.
    $this->server->setOption('allow_unverified_from_email', 'yes');
    $this->server->save();

    $campaign = new \App\Model\Campaign();
    $campaign->customer_id = $this->customer->id;
    $campaign->use_default_sending_server_from_email = false;
    $campaign->setRelation('customer', $this->customer);

    $rules = $campaign->rules();
    $validator = \Illuminate\Support\Facades\Validator::make(
        ['from_email' => '[email protected]', 'name' => 'X', 'subject' => 'S',
         'from_name' => 'N', 'reply_to' => '[email protected]'],
        $rules
    );

    expect($validator->errors()->has('from_email'))->toBeFalse();
});

test('Campaign::rules from_email skips closure when use_default_sending_server_from_email=true', function () {
    // Server's own default is substituted at send time — the from_email
    // field is just metadata; gate doesn't apply.
    $campaign = new \App\Model\Campaign();
    $campaign->customer_id = $this->customer->id;
    $campaign->use_default_sending_server_from_email = true;
    $campaign->setRelation('customer', $this->customer);

    $rules = $campaign->rules();
    $validator = \Illuminate\Support\Facades\Validator::make(
        ['from_email' => '[email protected]', 'name' => 'X', 'subject' => 'S',
         'from_name' => 'N', 'reply_to' => '[email protected]'],
        $rules
    );

    expect($validator->errors()->has('from_email'))->toBeFalse();
});