trypost/tests/Feature/Onboarding/OnboardingControllerTest.php
Paulo Castellano 17a091975d fix: merge-readiness — close two billing/network bugs, harden tests
Bugs (both with regression tests):
- OnboardingController::store now guards already-subscribed accounts (mirrors
  index), preventing a second Stripe Checkout / double subscription if a
  subscribed user re-POSTs /onboarding.
- SocialAccountObserver: drop the `platform_user_id != …` clause from the
  creating-time one-per-network check. On create there is no "self" to exclude,
  so it only weakened the rule — the same account connected via two network
  variants (e.g. Instagram standalone + via Facebook, same id) could slip a
  second account into the network. Now any account in the network blocks.

Robustness:
- CreateWorkspace wraps create + member attach + switchWorkspace in a
  transaction (cache-forget / quantity-sync run after), so a partial failure
  can't leave an orphan workspace that inflates the Stripe seat count — covers
  both the signup and the add-workspace paths.

Test honesty & coverage:
- Scope the ten "connect multiple <platform> accounts" tests to self-hosted
  mode (config + name); they only passed because the test env defaults
  SELF_HOSTED=true, and in cloud the one-per-network rule blocks them.
- network_taken popup now has controller-level tests on all six OAuth
  controllers (added Threads, YouTube, LinkedInPage, and a new
  InstagramFacebook test file; LinkedInPage/InstagramFacebook also exercise
  variant collapse).
- Wiring tests that creating/deleting a workspace actually calls
  syncWorkspaceQuantity (guards per-seat billing against silent breakage).
- Strengthen TrialLengthTest to assert the configured length reaches
  trial_ends_at; add a same-id network-variant block test.
2026-06-22 09:26:31 -03:00

119 lines
4 KiB
PHP

<?php
declare(strict_types=1);
use App\Actions\Billing\StartSubscriptionCheckout;
use App\Enums\Plan\Slug;
use App\Enums\User\Persona;
use App\Jobs\PostHog\SendEvent;
use App\Models\Account;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Support\Facades\Bus;
beforeEach(function () {
config(['trypost.self_hosted' => false]);
$this->user = User::factory()->create();
});
test('onboarding renders the persona selection for an unsubscribed account', function () {
$response = $this->actingAs($this->user)->get(route('app.onboarding'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('onboarding/Index')
->has('personas', count(Persona::cases()))
);
});
test('onboarding redirects to calendar in self-hosted mode', function () {
config(['trypost.self_hosted' => true]);
$response = $this->actingAs($this->user)->get(route('app.onboarding'));
$response->assertRedirect(route('app.calendar'));
});
test('onboarding redirects to calendar when already subscribed', function () {
$this->user->account->subscriptions()->create([
'type' => Account::SUBSCRIPTION_NAME,
'stripe_id' => 'sub_'.fake()->uuid(),
'stripe_status' => 'active',
'stripe_price' => 'price_123',
]);
$response = $this->actingAs($this->user->fresh())->get(route('app.onboarding'));
$response->assertRedirect(route('app.calendar'));
});
test('onboarding store rejects an invalid persona', function () {
$response = $this->actingAs($this->user)->post(route('app.onboarding.store'), [
'persona' => 'not-a-persona',
]);
$response->assertSessionHasErrors('persona');
expect($this->user->fresh()->persona)->toBeNull();
});
test('onboarding store requires a persona', function () {
$response = $this->actingAs($this->user)->post(route('app.onboarding.store'), []);
$response->assertSessionHasErrors('persona');
});
test('onboarding store does nothing in self-hosted mode', function () {
config(['trypost.self_hosted' => true]);
$response = $this->actingAs($this->user)->post(route('app.onboarding.store'), [
'persona' => Persona::Agency->value,
]);
$response->assertRedirect(route('app.calendar'));
expect($this->user->fresh()->persona)->toBeNull();
});
test('onboarding store saves the persona, mirrors to PostHog and starts monthly checkout', function () {
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test']);
Bus::fake();
Plan::where('slug', Slug::Workspace)->firstOrFail()->update([
'stripe_monthly_price_id' => 'price_monthly_test',
'stripe_yearly_price_id' => 'price_yearly_test',
]);
$this->mock(StartSubscriptionCheckout::class)
->shouldReceive('redirect')
->once()
->withArgs(fn (Account $account, string $priceId, string $cancelUrl): bool => $priceId === 'price_monthly_test')
->andReturn(redirect()->route('app.calendar'));
$response = $this->actingAs($this->user)->post(route('app.onboarding.store'), [
'persona' => Persona::Agency->value,
]);
$response->assertRedirect(route('app.calendar'));
expect($this->user->fresh()->persona)->toBe(Persona::Agency);
Bus::assertDispatched(SendEvent::class);
});
test('onboarding store redirects an already-subscribed account to the calendar without starting a second checkout', function () {
$this->user->account->subscriptions()->create([
'type' => Account::SUBSCRIPTION_NAME,
'stripe_id' => 'sub_'.fake()->uuid(),
'stripe_status' => 'active',
'stripe_price' => 'price_123',
]);
$this->mock(StartSubscriptionCheckout::class)
->shouldReceive('redirect')
->never();
$response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.store'), [
'persona' => Persona::Agency->value,
]);
$response->assertRedirect(route('app.calendar'));
expect($this->user->fresh()->persona)->toBeNull();
});