File: /home/xedaptot/ai.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) {}
}
}
}