organisation = Organisation::factory()->create(); $this->event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'published', ]); } // --- Token Validation --- public function test_invalid_token_returns_401(): void { $response = $this->postJson('/api/v1/portal/token-auth', [ 'token' => 'completely-invalid-token-value', ]); $response->assertStatus(401); $response->assertJson(['message' => 'Invalid or expired portal token']); } public function test_empty_token_returns_422(): void { $response = $this->postJson('/api/v1/portal/token-auth', [ 'token' => '', ]); $response->assertUnprocessable(); } public function test_missing_token_returns_422(): void { $response = $this->postJson('/api/v1/portal/token-auth', []); $response->assertUnprocessable(); } // --- Response Shape --- public function test_valid_artist_token_returns_safe_response(): void { $plainToken = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $plainToken); DB::table('artists')->insert([ 'id' => strtolower((string) Str::ulid()), 'event_id' => $this->event->id, 'name' => 'Test Artist', 'booking_status' => 'confirmed', 'star_rating' => 3, 'project_leader_id' => null, 'milestone_offer_in' => true, 'milestone_offer_agreed' => true, 'milestone_confirmed' => true, 'milestone_announced' => false, 'milestone_schedule_confirmed' => false, 'milestone_itinerary_sent' => false, 'milestone_advance_sent' => false, 'milestone_advance_received' => false, 'portal_token' => $hashedToken, 'created_at' => now(), 'updated_at' => now(), ]); $response = $this->postJson('/api/v1/portal/token-auth', [ 'token' => $plainToken, ]); $response->assertOk(); $response->assertJsonStructure([ 'context', 'data' => ['id', 'name', 'booking_status'], 'event' => ['id', 'name', 'slug', 'start_date', 'end_date', 'status', 'event_type'], ]); // Must NOT contain internal fields $data = $response->json('data'); $this->assertArrayNotHasKey('star_rating', $data); $this->assertArrayNotHasKey('project_leader_id', $data); $this->assertArrayNotHasKey('milestone_offer_in', $data); $this->assertArrayNotHasKey('milestone_confirmed', $data); $this->assertArrayNotHasKey('portal_token', $data); $this->assertArrayNotHasKey('advance_open_from', $data); // Event must NOT contain internal fields $eventData = $response->json('event'); $this->assertArrayNotHasKey('organisation_id', $eventData); $this->assertArrayNotHasKey('recurrence_rule', $eventData); $this->assertArrayNotHasKey('recurrence_exceptions', $eventData); $this->assertArrayNotHasKey('registration_banner_url', $eventData); } public function test_token_lookup_uses_hash_not_plaintext(): void { $plainToken = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $plainToken); DB::table('artists')->insert([ 'id' => strtolower((string) Str::ulid()), 'event_id' => $this->event->id, 'name' => 'Hash Test Artist', 'booking_status' => 'concept', 'star_rating' => 1, 'portal_token' => $hashedToken, 'created_at' => now(), 'updated_at' => now(), ]); // Sending the hash directly should NOT work (must send plain token) $this->postJson('/api/v1/portal/token-auth', ['token' => $hashedToken]) ->assertStatus(401); // Sending the plain token should work (controller hashes it) $this->postJson('/api/v1/portal/token-auth', ['token' => $plainToken]) ->assertOk(); } // --- Generic Error Messages --- public function test_error_message_does_not_leak_info(): void { $response = $this->postJson('/api/v1/portal/token-auth', [ 'token' => 'this-token-does-not-exist', ]); $response->assertStatus(401); // Message should be generic — not "token not found in artists table" $message = $response->json('message'); $this->assertStringNotContainsString('artists', $message); $this->assertStringNotContainsString('table', $message); $this->assertStringNotContainsString('database', $message); } // --- Portal Middleware --- public function test_portal_token_middleware_returns_401_without_token(): void { // If any route uses portal.token middleware, it should reject without token. // We test the middleware directly via a simulated request. $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); $request = \Illuminate\Http\Request::create('/portal/test', 'GET'); $response = $middleware->handle($request, fn () => response()->json(['ok' => true])); $this->assertEquals(401, $response->getStatusCode()); } public function test_portal_token_middleware_rejects_invalid_token(): void { $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); $request = \Illuminate\Http\Request::create('/portal/test', 'GET', ['token' => 'fake-token']); $response = $middleware->handle($request, fn () => response()->json(['ok' => true])); $this->assertEquals(401, $response->getStatusCode()); } public function test_portal_token_middleware_accepts_valid_token(): void { $plainToken = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $plainToken); DB::table('artists')->insert([ 'id' => strtolower((string) Str::ulid()), 'event_id' => $this->event->id, 'name' => 'Middleware Test', 'booking_status' => 'confirmed', 'star_rating' => 1, 'portal_token' => $hashedToken, 'created_at' => now(), 'updated_at' => now(), ]); $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); $request = \Illuminate\Http\Request::create('/portal/test', 'GET', [], [], [], [ 'HTTP_AUTHORIZATION' => "Bearer {$plainToken}", ]); $nextCalled = false; $response = $middleware->handle($request, function ($req) use (&$nextCalled) { $nextCalled = true; $this->assertEquals('artist', $req->attributes->get('portal_context')); $this->assertNotNull($req->attributes->get('portal_event')); return response()->json(['ok' => true]); }); $this->assertTrue($nextCalled); $this->assertEquals(200, $response->getStatusCode()); } public function test_portal_token_middleware_rejects_draft_event(): void { $draftEvent = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); $plainToken = bin2hex(random_bytes(32)); DB::table('artists')->insert([ 'id' => strtolower((string) Str::ulid()), 'event_id' => $draftEvent->id, 'name' => 'Draft Event Artist', 'booking_status' => 'concept', 'star_rating' => 1, 'portal_token' => hash('sha256', $plainToken), 'created_at' => now(), 'updated_at' => now(), ]); $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); $request = \Illuminate\Http\Request::create('/portal/test', 'GET', ['token' => $plainToken]); $response = $middleware->handle($request, fn () => response()->json(['ok' => true])); $this->assertEquals(401, $response->getStatusCode()); } }