tokenService = app(EventJoinTokenService::class); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } private function createPublishedEvent(): Event { return Event::factory()->create([ 'status' => 'published', ]); } public function test_guest_can_access_stats_using_join_token(): void { $event = $this->createPublishedEvent(); Photo::factory()->count(3)->create([ 'event_id' => $event->id, 'guest_name' => 'device-stats', ]); $token = $this->tokenService->createToken($event); $response = $this->getJson("/api/v1/events/{$token->token}/stats"); $response->assertOk() ->assertJsonStructure([ 'online_guests', 'tasks_solved', 'latest_photo_at', ]); } public function test_guest_can_upload_photo_with_join_token(): void { Storage::fake('public'); $event = $this->createPublishedEvent(); $package = Package::factory()->endcustomer()->create([ 'max_photos' => 100, ]); EventPackage::create([ 'event_id' => $event->id, 'package_id' => $package->id, 'purchased_price' => $package->price, 'purchased_at' => now(), 'used_photos' => 0, 'used_guests' => 0, ]); MediaStorageTarget::create([ 'key' => 'public', 'name' => 'Public', 'driver' => 'local', 'is_hot' => true, 'is_default' => true, 'is_active' => true, ]); $token = $this->tokenService->createToken($event); Mockery::mock('alias:App\Support\ImageHelper') ->shouldReceive('makeThumbnailOnDisk') ->andReturn("events/{$event->id}/photos/thumbs/generated_thumb.jpg") ->shouldReceive('copyWithWatermark') ->andReturnNull(); $file = UploadedFile::fake()->image('example.jpg', 1200, 800); $response = $this->withHeader('X-Device-Id', 'token-device') ->postJson("/api/v1/events/{$token->token}/upload", [ 'photo' => $file, ]); $response->assertCreated() ->assertJsonStructure(['id', 'status', 'message']); $this->assertDatabaseCount('photos', 1); $saved = Photo::first(); $this->assertNotNull($saved); $this->assertEquals($event->id, $saved->event_id); $storedPath = $saved->file_path ? ltrim(str_replace('/storage/', '', $saved->file_path), '/') : null; if ($storedPath) { $this->assertTrue( Storage::disk('public')->exists($storedPath), sprintf('Uploaded file [%s] was not stored on the public disk.', $storedPath) ); } } public function test_guest_event_response_includes_demo_read_only_flag(): void { $event = $this->createPublishedEvent(); $token = $this->tokenService->createToken($event, [ 'metadata' => ['demo_read_only' => true], ]); $response = $this->getJson("/api/v1/events/{$token->token}"); $response->assertOk() ->assertJsonPath('demo_read_only', true); } public function test_guest_cannot_upload_photo_with_demo_token(): void { Storage::fake('public'); $event = $this->createPublishedEvent(); $token = $this->tokenService->createToken($event, [ 'metadata' => ['demo_read_only' => true], ]); $file = UploadedFile::fake()->image('example.jpg', 1200, 800); $response = $this->withHeader('X-Device-Id', 'token-device') ->postJson("/api/v1/events/{$token->token}/upload", [ 'photo' => $file, ]); $response->assertStatus(403) ->assertJsonPath('error.code', 'demo_read_only'); $this->assertDatabaseCount('photos', 0); } public function test_guest_can_like_photo_after_joining_with_token(): void { $event = $this->createPublishedEvent(); $token = $this->tokenService->createToken($event); $photo = Photo::factory()->create([ 'event_id' => $event->id, 'likes_count' => 0, ]); $this->getJson("/api/v1/events/{$token->token}"); $response = $this->withHeader('X-Device-Id', 'device-like') ->postJson("/api/v1/photos/{$photo->id}/like"); $response->assertOk() ->assertJson([ 'liked' => true, ]); $this->assertDatabaseHas('photo_likes', [ 'photo_id' => $photo->id, 'guest_name' => 'device-like', ]); $this->assertEquals(1, $photo->fresh()->likes_count); } public function test_guest_cannot_access_event_with_expired_token(): void { $event = $this->createPublishedEvent(); $token = $this->tokenService->createToken($event, [ 'expires_at' => now()->subDay(), ]); $response = $this->getJson("/api/v1/events/{$token->token}"); $response->assertStatus(410) ->assertJsonPath('error.code', 'token_expired'); } public function test_slug_access_is_rejected(): void { $event = $this->createPublishedEvent(); $response = $this->getJson("/api/v1/events/{$event->slug}"); $response->assertStatus(404) ->assertJsonPath('error.code', 'invalid_token'); } public function test_guest_cannot_access_event_with_revoked_token(): void { $event = $this->createPublishedEvent(); $token = $this->tokenService->createToken($event); $this->tokenService->revoke($token, 'revoked for test'); $response = $this->getJson("/api/v1/events/{$token->token}"); $response->assertStatus(410) ->assertJsonPath('error.code', 'token_revoked'); } }