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/RemoteSubscription/StripeIntegrationTest.php
<?php

namespace Tests\Unit\RemoteSubscription;

use Tests\TestCase;
use App\Cashier\Services\StripeSubscriptionGateway;
use App\Cashier\DTO\RemotePlanDTO;
use App\Cashier\DTO\RemoteSubscriptionDTO;

/**
 * Live integration tests against Stripe Test API.
 *
 * These tests require valid Stripe test-mode API keys and
 * actual test-mode products/prices in the Stripe account.
 *
 * Run selectively:
 *   php artisan test --filter=StripeIntegrationTest
 *   vendor/bin/pest --filter=StripeIntegrationTest
 *
 * To skip in CI, set env SKIP_STRIPE_LIVE_TESTS=true
 */
class StripeIntegrationTest extends TestCase
{
    private StripeSubscriptionGateway $gateway;

    protected function setUp(): void
    {
        parent::setUp();

        if (env('SKIP_STRIPE_LIVE_TESTS', false)) {
            $this->markTestSkipped('Skipping live Stripe tests (SKIP_STRIPE_LIVE_TESTS=true)');
        }

        $secretKey = env('STRIPE_TEST_SECRET_KEY', 'sk_test_rQwQAGrsSGiqFJywxeMLBRy8');
        $publishableKey = env('STRIPE_TEST_PUBLISHABLE_KEY', 'pk_test_fqKGTiyNkzQB2muKmYG6HzOM');

        if (!$secretKey || !str_starts_with($secretKey, 'sk_test_')) {
            $this->markTestSkipped('No valid Stripe test secret key available');
        }

        $this->gateway = new StripeSubscriptionGateway($publishableKey, $secretKey);
    }

    // ==========================================
    // getRemotePlans
    // ==========================================

    public function test_get_remote_plans_returns_array()
    {
        $plans = $this->gateway->getRemotePlans();

        $this->assertIsArray($plans);

        // If there are plans, verify DTO structure
        if (!empty($plans)) {
            $first = $plans[0];
            $this->assertInstanceOf(RemotePlanDTO::class, $first);
            $this->assertNotEmpty($first->id);
            $this->assertNotEmpty($first->name);
            $this->assertIsFloat($first->price);
            $this->assertNotEmpty($first->currency);
            $this->assertIsInt($first->intervalCount);
            $this->assertNotEmpty($first->intervalUnit);
            $this->assertNotEmpty($first->status);
            $this->assertIsArray($first->metadata);
            $this->assertArrayHasKey('stripe_product_id', $first->metadata);
            $this->assertArrayHasKey('stripe_price_id', $first->metadata);
        }
    }

    public function test_remote_plans_are_all_recurring()
    {
        $plans = $this->gateway->getRemotePlans();

        foreach ($plans as $plan) {
            $this->assertContains($plan->intervalUnit, ['day', 'week', 'month', 'year'],
                "Plan {$plan->id} has unexpected interval: {$plan->intervalUnit}");
            $this->assertGreaterThan(0, $plan->intervalCount,
                "Plan {$plan->id} has non-positive interval count");
        }
    }

    public function test_remote_plans_have_valid_currencies()
    {
        $plans = $this->gateway->getRemotePlans();

        foreach ($plans as $plan) {
            $this->assertMatchesRegularExpression('/^[A-Z]{3}$/', $plan->currency,
                "Plan {$plan->id} currency '{$plan->currency}' is not a valid 3-letter uppercase code");
        }
    }

    public function test_remote_plan_summary_format()
    {
        $plans = $this->gateway->getRemotePlans();

        if (empty($plans)) {
            $this->markTestSkipped('No remote plans available to test summary format');
        }

        $first = $plans[0];
        $summary = $first->summary();

        $this->assertStringContainsString($first->name, $summary);
        $this->assertStringContainsString($first->currency, $summary);
        $this->assertStringContainsString((string)$first->intervalCount, $summary);
        $this->assertStringContainsString($first->intervalUnit, $summary);
    }

    // ==========================================
    // getRemotePlan (single)
    // ==========================================

    public function test_get_remote_plan_by_id()
    {
        $allPlans = $this->gateway->getRemotePlans();

        if (empty($allPlans)) {
            $this->markTestSkipped('No remote plans in Stripe test account');
        }

        $targetId = $allPlans[0]->id;
        $plan = $this->gateway->getRemotePlan($targetId);

        $this->assertInstanceOf(RemotePlanDTO::class, $plan);
        $this->assertEquals($targetId, $plan->id);
        $this->assertEquals($allPlans[0]->price, $plan->price);
        $this->assertEquals($allPlans[0]->currency, $plan->currency);
        $this->assertEquals($allPlans[0]->intervalCount, $plan->intervalCount);
        $this->assertEquals($allPlans[0]->intervalUnit, $plan->intervalUnit);
    }

    public function test_get_remote_plan_throws_for_invalid_id()
    {
        $this->expectException(\Stripe\Exception\InvalidRequestException::class);

        $this->gateway->getRemotePlan('price_nonexistent_12345');
    }

    // ==========================================
    // getRemoteSubscription
    // ==========================================

    public function test_get_remote_subscription_throws_for_invalid_id()
    {
        $this->expectException(\Stripe\Exception\InvalidRequestException::class);

        $this->gateway->getRemoteSubscription('sub_nonexistent_12345');
    }

    // ==========================================
    // Gateway initialization
    // ==========================================

    public function test_gateway_is_active_with_valid_keys()
    {
        $this->assertTrue($this->gateway->isActive());
    }

    public function test_gateway_is_inactive_with_empty_keys()
    {
        $gateway = new StripeSubscriptionGateway('', '');
        $this->assertFalse($gateway->isActive());
    }

    public function test_publishable_key_is_accessible()
    {
        $key = $this->gateway->getPublishableKey();
        $this->assertStringStartsWith('pk_test_', $key);
    }

    // ==========================================
    // Trial days extraction
    // ==========================================

    public function test_trial_days_is_null_or_int()
    {
        $plans = $this->gateway->getRemotePlans();

        foreach ($plans as $plan) {
            $this->assertTrue(
                $plan->trialDays === null || is_int($plan->trialDays),
                "Plan {$plan->id} trialDays should be null or int, got: " . gettype($plan->trialDays)
            );
        }
    }

    // ==========================================
    // Consistency: list vs single fetch
    // ==========================================

    public function test_list_and_single_fetch_return_consistent_data()
    {
        $allPlans = $this->gateway->getRemotePlans();

        if (empty($allPlans)) {
            $this->markTestSkipped('No remote plans available');
        }

        // Fetch first plan individually and compare
        $fromList = $allPlans[0];
        $fromSingle = $this->gateway->getRemotePlan($fromList->id);

        $this->assertEquals($fromList->id, $fromSingle->id);
        $this->assertEquals($fromList->name, $fromSingle->name);
        $this->assertEquals($fromList->price, $fromSingle->price);
        $this->assertEquals($fromList->currency, $fromSingle->currency);
        $this->assertEquals($fromList->intervalCount, $fromSingle->intervalCount);
        $this->assertEquals($fromList->intervalUnit, $fromSingle->intervalUnit);
        $this->assertEquals($fromList->trialDays, $fromSingle->trialDays);
        $this->assertEquals($fromList->status, $fromSingle->status);
    }

    // ==========================================
    // Live subscription lifecycle tests
    // ==========================================

    /**
     * Create a real Stripe customer + subscription using a test payment method,
     * then verify fetch, DTO fields, and cleanup by cancellation.
     */
    public function test_create_fetch_cancel_subscription_lifecycle()
    {
        $plans = $this->gateway->getRemotePlans();
        if (empty($plans)) {
            $this->markTestSkipped('No remote plans in Stripe test account');
        }

        $secretKey = env('STRIPE_TEST_SECRET_KEY', 'sk_test_rQwQAGrsSGiqFJywxeMLBRy8');
        \Stripe\Stripe::setApiKey($secretKey);

        // Create a test customer
        $customer = \Stripe\Customer::create([
            'email' => 'test-lifecycle-' . time() . '@example.com',
            'name' => 'Test Lifecycle Customer',
        ]);

        // Create a test payment method (using Stripe test token)
        $pm = \Stripe\PaymentMethod::create([
            'type' => 'card',
            'card' => ['token' => 'tok_visa'],
        ]);
        $pm->attach(['customer' => $customer->id]);

        // Set default payment method
        \Stripe\Customer::update($customer->id, [
            'invoice_settings' => ['default_payment_method' => $pm->id],
        ]);

        $planId = $plans[0]->id;

        // Create a subscription directly via Stripe API
        $stripeSub = \Stripe\Subscription::create([
            'customer' => $customer->id,
            'items' => [['price' => $planId]],
            'default_payment_method' => $pm->id,
        ]);

        try {
            // Fetch via our gateway
            $remoteSub = $this->gateway->getRemoteSubscription($stripeSub->id);

            // Verify DTO
            $this->assertInstanceOf(RemoteSubscriptionDTO::class, $remoteSub);
            $this->assertEquals($stripeSub->id, $remoteSub->id);
            $this->assertTrue($remoteSub->isActive());
            $this->assertFalse($remoteSub->isCanceled());
            $this->assertFalse($remoteSub->isIncomplete());
            $this->assertFalse($remoteSub->isTerminal());
            $this->assertFalse($remoteSub->hasIssue());
            $this->assertEquals($planId, $remoteSub->remotePlanId);
            $this->assertEquals($customer->id, $remoteSub->remoteCustomerId);
            $this->assertNotNull($remoteSub->currentPeriodEnd);
            $this->assertNotNull($remoteSub->currentPeriodStart);
            $this->assertNull($remoteSub->canceledAt);

            // Cancel the subscription (our method sets cancel_at_period_end=true)
            $this->gateway->cancelRemoteSubscription($stripeSub->id);

            // Fetch again — status should still be active (cancel at period end)
            $pendingCancel = $this->gateway->getRemoteSubscription($stripeSub->id);
            $this->assertTrue($pendingCancel->isActive()); // still active until period end
            $this->assertNotNull($pendingCancel->canceledAt); // scheduled to cancel

            // Now force-cancel immediately via Stripe API for full lifecycle test
            \Stripe\Subscription::retrieve($stripeSub->id)->cancel();

            $canceled = $this->gateway->getRemoteSubscription($stripeSub->id);
            $this->assertTrue($canceled->isCanceled());
            $this->assertTrue($canceled->isTerminal());
            $this->assertNotNull($canceled->canceledAt);
        } finally {
            // Cleanup: ensure subscription is canceled
            try {
                \Stripe\Subscription::update($stripeSub->id, ['cancel_at_period_end' => false]);
                $stripeSub->cancel();
            } catch (\Throwable $e) {
                // Already canceled
            }
            // Cleanup customer
            try {
                $customer->delete();
            } catch (\Throwable $e) {
                // Non-critical
            }
        }
    }

    /**
     * Test that the sync service correctly processes a live Stripe subscription.
     */
    public function test_live_subscription_dto_status_helpers()
    {
        $plans = $this->gateway->getRemotePlans();
        if (empty($plans)) {
            $this->markTestSkipped('No remote plans in Stripe test account');
        }

        $secretKey = env('STRIPE_TEST_SECRET_KEY', 'sk_test_rQwQAGrsSGiqFJywxeMLBRy8');
        \Stripe\Stripe::setApiKey($secretKey);

        $customer = \Stripe\Customer::create([
            'email' => 'test-dto-' . time() . '@example.com',
        ]);

        $pm = \Stripe\PaymentMethod::create(['type' => 'card', 'card' => ['token' => 'tok_visa']]);
        $pm->attach(['customer' => $customer->id]);

        $stripeSub = \Stripe\Subscription::create([
            'customer' => $customer->id,
            'items' => [['price' => $plans[0]->id]],
            'default_payment_method' => $pm->id,
        ]);

        try {
            $dto = $this->gateway->getRemoteSubscription($stripeSub->id);

            // Active subscription assertions
            $this->assertTrue($dto->isActive());
            $this->assertFalse($dto->isTrialing());
            $this->assertFalse($dto->isPastDue());
            $this->assertFalse($dto->isCanceled());
            $this->assertFalse($dto->isIncomplete());
            $this->assertFalse($dto->isIncompleteExpired());
            $this->assertFalse($dto->isPaused());
            $this->assertFalse($dto->isTerminal());
            $this->assertFalse($dto->hasIssue());
        } finally {
            try { $stripeSub->cancel(); } catch (\Throwable $e) {}
            try { $customer->delete(); } catch (\Throwable $e) {}
        }
    }
}