withoutMiddleware(EnsurePlatformSession::class); $this->user = User::create([ 'public_id' => 'phase7-user-001', 'name' => 'Phase7 User', 'email' => 'phase7@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->user->public_id, 'name' => 'Phase7 Org', 'slug' => 'phase7-org', 'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8], ]); Member::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->user->public_id, 'role' => 'org_admin', ]); Branch::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'name' => 'HQ', 'is_active' => true, ]); Host::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'name' => 'Lobby Host', 'is_available' => true, ]); $this->deviceToken = 'kiosk-test-token-'.str_repeat('x', 32); } public function test_admin_can_register_kiosk_device(): void { $this->actingAs($this->user) ->post(route('frontdesk.devices.store'), [ 'name' => 'Lobby Kiosk', 'type' => 'kiosk', ]) ->assertRedirect(route('frontdesk.devices.index')); $device = Device::first(); $this->assertSame('kiosk', $device->type); $this->assertNotEmpty($device->device_token); } public function test_device_create_page_shows_type_specific_registration_hints(): void { $response = $this->actingAs($this->user)->get(route('frontdesk.devices.create')); $response->assertOk(); $response->assertSee('Tracked for your team only', false); $response->assertSee('Open the kiosk URL on your tablet', false); $response->assertSee('x-model="type"', false); $response->assertDontSee('Kiosks and printers receive a device token for unattended access.', false); } public function test_reception_computer_registration_does_not_issue_token(): void { $this->actingAs($this->user) ->post(route('frontdesk.devices.store'), [ 'name' => 'Front desk PC', 'type' => 'reception_computer', ]) ->assertRedirect(route('frontdesk.devices.index')); $device = Device::first(); $this->assertSame('reception_computer', $device->type); $this->assertNull($device->device_token); } public function test_kiosk_device_page_works_without_auth(): void { $device = $this->createKioskDevice(); $this->get(route('frontdesk.kiosk.device', $device->device_token)) ->assertOk() ->assertSee('Visitor Check-in') ->assertSee('Welcome to') ->assertSee('Tap to begin'); $this->assertTrue($device->fresh()->isOnline()); } public function test_kiosk_device_page_shows_custom_organization_logo(): void { Storage::fake('public'); $path = UploadedFile::fake()->image('brand.png', 320, 80)->store('frontdesk/organizations/'.$this->organization->id, 'public'); $this->organization->update(['logo_path' => $path]); $device = $this->createKioskDevice(); $this->get(route('frontdesk.kiosk.device', $device->device_token)) ->assertOk() ->assertSee(Storage::disk('public')->url($path), false); } public function test_kiosk_device_can_check_in_visitor(): void { $device = $this->createKioskDevice(); $host = Host::first(); $this->postJson(route('frontdesk.kiosk.device.check-in', $device->device_token), [ 'full_name' => 'Kiosk Guest', 'host_id' => $host->id, 'visitor_type' => 'visitor', 'policies_accepted' => 1, ])->assertOk() ->assertJsonPath('visit.visitor_name', 'Kiosk Guest'); $visit = Visit::first(); $this->assertSame(Visit::STATUS_CHECKED_IN, $visit->status); $this->assertSame($device->branch_id, $visit->branch_id); } public function test_device_heartbeat_api_marks_online(): void { $device = $this->createKioskDevice(['status' => 'offline', 'last_online_at' => null]); $this->postJson('/api/devices/heartbeat', [], [ 'X-Device-Token' => $device->device_token, ])->assertOk() ->assertJsonPath('status', 'online'); $this->assertTrue($device->fresh()->isOnline()); } public function test_mark_devices_offline_command(): void { Carbon::setTestNow(now()); $device = $this->createKioskDevice([ 'status' => 'online', 'last_online_at' => now()->subMinutes(30), ]); $this->artisan('frontdesk:mark-devices-offline')->assertSuccessful(); $this->assertSame('offline', $device->fresh()->status); Carbon::setTestNow(); } public function test_invalid_device_token_returns_not_found(): void { $this->get(route('frontdesk.kiosk.device', 'invalid-token')) ->assertNotFound(); } /** @param array $overrides */ protected function createKioskDevice(array $overrides = []): Device { return Device::create(array_merge([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => Branch::first()->id, 'name' => 'Test Kiosk', 'type' => 'kiosk', 'status' => 'offline', 'device_token' => $this->deviceToken, 'config' => ['mode' => 'self_service'], ], $overrides)); } }