create(['email' => 's@set.test', 'is_admin' => false]); $customer = Customer::factory()->create(['email' => 's@set.test', 'user_id' => $user->id, 'name' => 'Acme', 'status' => 'active']); if ($withInstance) { $order = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']); Instance::factory()->create(['customer_id' => $customer->id, 'order_id' => $order->id, 'plan' => 'team', 'status' => 'active', 'subdomain' => 'acme']); } return compact('user', 'customer'); } it('gates settings to authenticated users', function () { $this->get(route('settings'))->assertRedirect('/login'); }); it('blocks a suspended customer from the portal', function () { $user = User::factory()->create(['email' => 'sus@set.test', 'is_admin' => false]); Customer::factory()->create(['email' => 'sus@set.test', 'user_id' => $user->id, 'status' => 'suspended']); $this->actingAs($user)->get(route('dashboard'))->assertRedirect('/login'); expect(auth()->check())->toBeFalse(); }); it('blocks a closed customer from the portal', function () { $user = User::factory()->create(['email' => 'cl@set.test', 'is_admin' => false]); Customer::factory()->create(['email' => 'cl@set.test', 'user_id' => $user->id, 'status' => 'closed', 'closed_at' => now()]); $this->actingAs($user)->get(route('dashboard'))->assertRedirect('/login'); }); it('rejects cancelling a package that is not active', function () { ['user' => $user, 'customer' => $customer] = settingsSetup(); $instance = $customer->instances()->first(); $instance->update(['status' => 'cancellation_scheduled']); Livewire::actingAs($user)->test(ConfirmCancelPackage::class) ->set('confirmName', 'acme') ->call('cancelPackage'); // Still scheduled — not re-transitioned or resurrected. expect($instance->fresh()->status)->toBe('cancellation_scheduled'); }); it('saves the company profile', function () { ['user' => $user, 'customer' => $customer] = settingsSetup(); Livewire::actingAs($user)->test(Settings::class) ->set('companyName', 'Acme GmbH') ->set('vatId', 'ATU12345678') ->set('billingAddress', "Hauptstraße 1\n1010 Wien") ->call('saveProfile') ->assertHasNoErrors(); $customer->refresh(); expect($customer->name)->toBe('Acme GmbH') ->and($customer->vat_id)->toBe('ATU12345678') ->and($customer->billing_address)->toContain('Wien'); }); it('saves branding and resolves defaults when unset', function () { ['user' => $user, 'customer' => $customer] = settingsSetup(); // Unset → resolver reports defaults. expect($customer->brandingResolved()['is_default'])->toBeTrue(); Livewire::actingAs($user)->test(Settings::class) ->set('brandDisplayName', 'Acme Cloud') ->set('brandPrimary', '#123456') ->call('saveBranding') ->assertHasNoErrors(); $customer->refresh(); expect($customer->brand_display_name)->toBe('Acme Cloud') ->and($customer->brandingResolved()['primary_color'])->toBe('#123456') ->and($customer->brandingResolved()['is_default'])->toBeFalse(); }); it('rejects an invalid brand colour', function () { ['user' => $user] = settingsSetup(); Livewire::actingAs($user)->test(Settings::class) ->set('brandPrimary', 'notacolor') ->call('saveBranding') ->assertHasErrors(['brandPrimary']); }); it('schedules package cancellation only with the correct typed confirmation', function () { $stripe = new FakeStripeClient; app()->instance(StripeClient::class, $stripe); ['user' => $user, 'customer' => $customer] = settingsSetup(); $instance = $customer->instances()->first(); // A YEARLY contract, because the assertion below is a date and a monthly // assumption would land eleven months early. This test used to assert only // that `service_ends_at` was not null — a column the code had just written, // which the broken monthly arithmetic satisfied exactly as well as the right // answer does. $contract = Subscription::factory()->plan('team', Subscription::TERM_YEARLY)->create([ 'customer_id' => $customer->id, 'order_id' => $instance->order_id, 'instance_id' => $instance->id, 'stripe_subscription_id' => 'sub_settings', 'current_period_start' => '2026-01-01 00:00:00', 'current_period_end' => '2027-01-01 00:00:00', ]); // Wrong confirmation → no change, and Stripe is not told anything either. Livewire::actingAs($user)->test(ConfirmCancelPackage::class) ->set('confirmName', 'wrong') ->call('cancelPackage') ->assertHasErrors(['confirmName']); expect($instance->fresh()->status)->toBe('active') ->and($stripe->cancellations)->toBeEmpty(); // Correct confirmation → scheduled. Livewire::actingAs($user)->test(ConfirmCancelPackage::class) ->set('confirmName', 'acme') ->call('cancelPackage'); $instance->refresh(); expect($instance->status)->toBe('cancellation_scheduled') // The end of the term that was PAID FOR, stated as a date somebody chose. ->and($instance->service_ends_at->toDateTimeString())->toBe('2027-01-01 00:00:00'); // The contract knows it was cancelled — and is still active, because until // the first of January this is a paying customer with a running cloud. expect($contract->fresh()->cancel_requested_at)->not->toBeNull() ->and($contract->fresh()->status)->toBe('active'); // And Stripe was told to stop, at the period end and not now. expect($stripe->cancellations)->toHaveCount(1) ->and($stripe->cancellations[0]['subscription'])->toBe('sub_settings') ->and($stripe->cancellations[0]['when'])->toBe(StripeClient::CANCEL_AT_PERIOD_END); }); it('blocks closing the account while a package is active', function () { ['user' => $user, 'customer' => $customer] = settingsSetup(); Livewire::actingAs($user)->test(ConfirmCloseAccount::class) ->set('confirmWord', __('settings.close_keyword')) ->call('closeAccount') ->assertHasErrors(['confirmWord']); expect($customer->fresh()->closed_at)->toBeNull(); }); it('closes the account when no package is active', function () { ['user' => $user, 'customer' => $customer] = settingsSetup(withInstance: false); Livewire::actingAs($user)->test(ConfirmCloseAccount::class) ->set('confirmWord', __('settings.close_keyword')) ->call('closeAccount'); expect($customer->fresh()->closed_at)->not->toBeNull(); }); // ---- Four tabs, and the choice in the address ---- it('opens on the customer own details and says so in the address', function () { // The page was one column 768px wide inside a 1240px shell, holding the // company address, the password, two-factor, the devices, the branding and // the contract — two thousand pixels of scroll, and finding a thing meant // knowing how far down it lived. $user = App\Models\User::factory()->create(['email_verified_at' => now()]); Livewire::actingAs($user)->test(App\Livewire\Settings::class) ->assertSet('tab', 'profile') ->assertSee(__('settings.company_title')) // Not the other tabs' content. ->assertDontSee(__('settings.branding_title')) ->assertDontSee(__('settings.package_title')); }); it('lands on the tab the address names', function () { $user = App\Models\User::factory()->create(['email_verified_at' => now()]); Livewire::actingAs($user)->test(App\Livewire\Settings::class, ['tab' => 'branding']) ->assertSet('tab', 'branding') ->assertSee(__('settings.branding_title')) ->assertDontSee(__('settings.company_title')); }); it('falls back to the first tab when the address names one nobody has', function () { // A tab name out of the URL is a string a stranger typed; the alternative // is a settings page with no section on it at all. $user = App\Models\User::factory()->create(['email_verified_at' => now()]); Livewire::actingAs($user)->test(App\Livewire\Settings::class, ['tab' => 'gibt-es-nicht']) ->assertSet('tab', 'profile') ->assertSee(__('settings.company_title')); }); it('writes the tab into the query string, so a link can name one', function () { // history: true and no `except` — the parameter is there from the first // render. Hidden on the default, a link to this page could not say which // tab it meant unless the tab happened not to be the first. $component = Illuminate\Support\Facades\File::get(app_path('Livewire/Settings.php')); expect($component)->toContain('#[Url(history: true)]') // The property and its default, matched without spelling a PHP // variable inside a quoted string. ->and($component)->toContain('public string $tab') ->and($component)->toContain("= 'profile';") ->and($component)->not->toContain('except:'); }); it('keeps every tab reachable from the bar', function () { // The list IS the schema: it validates the query string, builds the bar and // decides which section renders. $user = App\Models\User::factory()->create(['email_verified_at' => now()]); $page = Livewire::actingAs($user)->test(App\Livewire\Settings::class); foreach (App\Livewire\Settings::TABS as $tab) { $page->assertSee(__('settings.tab.'.$tab)); } }); it('uses the width it is given', function () { // 768px in a 1240px shell left a third of the window empty beside a column // nobody could read the end of. expect(Illuminate\Support\Facades\File::get(resource_path('views/livewire/settings.blade.php'))) ->not->toContain('max-w-3xl'); });