get('/')->assertOk(); $this->get('/login')->assertOk(); }); it('shows outsiders a placeholder while the site is hidden', function () { Settings::set('site.public', false); $response = $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.50'])->get('/'); // 503, not 200: a 200 invites search engines to index the placeholder as // the site's content, and that is much harder to undo than to prevent. $response->assertStatus(503) ->assertSee(__('coming_soon.title')) ->assertHeader('X-Robots-Tag', 'noindex, nofollow, noarchive') ->assertHeader('Retry-After', 3600); // The portal is hidden too, not just the marketing page. $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.50'])->get('/login')->assertStatus(503); }); it('lets the management VPN through', function () { Settings::set('site.public', false); config()->set('admin_access.trusted_ranges', ['10.66.0.0/24']); $this->withServerVariables(['REMOTE_ADDR' => '10.66.0.4'])->get('/')->assertOk(); $this->withServerVariables(['REMOTE_ADDR' => '10.66.9.4'])->get('/')->assertStatus(503); }); it('lets a signed-in operator see the real site from anywhere', function () { Settings::set('site.public', false); $this->actingAs(operator('Support'), 'operator') ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->get('/') ->assertOk(); // Both matter: the operator guard instance keeps its user across requests // within one test regardless of which guard is "default", so it has to be // logged out explicitly — and the customer has to be put on 'web' // explicitly, because a bare actingAs() would target whatever guard is // still default (still 'operator', from the call above) rather than reset // to it. Auth::guard('operator')->logout(); // A customer account is not staff — the portal stays hidden for them. $this->actingAs(User::factory()->create(), 'web') ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->get('/') ->assertStatus(503); }); it('does not let a disabled operator bypass the hidden site', function () { // EnsureAdmin already refuses a disabled operator (isActive()); this gate // used to check only Auth::guard('operator')->check(), which stays true // for a disabled operator's live session — an indefinite bypass of // maintenance/private-site mode for anyone disabled while signed in. Settings::set('site.public', false); $op = operator('Support'); $op->update(['disabled_at' => now()]); $this->actingAs($op, 'operator') ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->get('/') ->assertStatus(503); // Chosen deliberately: merely ignored, not logged out — see // PublicSiteGate's docblock for why (this gate only decides what ONE // request sees; logging out would ripple into whatever else that // session is doing, e.g. a concurrent console tab). expect(Auth::guard('operator')->check())->toBeTrue(); }); it('does not let an operator holding no console role bypass the hidden site', function () { // Mirrors EnsureAdmin's other half: isActive() alone is not enough, // the operator must also hold a role that reaches the console. Settings::set('site.public', false); $op = Operator::factory()->create(); // no role assigned $this->actingAs($op, 'operator') ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->get('/') ->assertStatus(503); }); it('keeps the console and the webhook reachable while hidden', function () { Settings::set('site.public', false); // Otherwise the switch could only ever be flipped once. $this->actingAs(operator('Owner'), 'operator') ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->get(route('admin.settings')) ->assertOk(); // Stripe must still be able to deliver a paid order: whatever the webhook // makes of an empty body, the gate must not be what answers. $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->post('/webhooks/stripe', []) ->assertStatus(200); }); it('tells crawlers to stay away while hidden', function () { Settings::set('site.public', false); $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->get('/robots.txt') ->assertOk() ->assertSee('Disallow: /'); Settings::set('site.public', true); $this->get('/robots.txt')->assertOk()->assertSee('Allow: /'); }); it('flips visibility from the console, and only with the capability', function () { Settings::set('site.public', true); Livewire\Livewire::actingAs(operator('Support'), 'operator')->test(App\Livewire\Admin\Settings::class) ->call('toggleSiteVisibility') ->assertForbidden(); expect(Settings::bool('site.public', true))->toBeTrue(); Livewire\Livewire::actingAs(operator('Owner'), 'operator')->test(App\Livewire\Admin\Settings::class) ->call('toggleSiteVisibility'); expect(Settings::bool('site.public', true))->toBeFalse(); }); it('keeps serving when the settings table is unavailable', function () { // A deploy that runs new code before migrations, or a database blip: the // gate must fall back to "public", not take the whole site down with a 500. Schema::drop('app_settings'); $this->get('/')->assertOk(); }); it('does not leave the portal drivable through Livewire while hidden', function () { Settings::set('site.public', false); // A customer with a live session must not be able to keep using the portal // through the shared Livewire endpoint while it is supposed to be offline. $this->actingAs(User::factory()->create()) ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->post('/livewire/update', []) ->assertStatus(503); // Operators keep working — that is what the console needs. Whatever // Livewire makes of an empty payload, the gate must not be what answers. $status = $this->actingAs(operator('Owner'), 'operator') ->withServerVariables(['REMOTE_ADDR' => '203.0.113.50']) ->post('/livewire/update', []) ->getStatusCode(); expect($status)->not->toBe(503); });