seed(CityWorldSeeder::class); $this->user = User::factory()->create(['status' => User::STATUS_ACTIVE, 'is_active' => true]); $this->character = Character::ensureForUser($this->user); $this->actingAs($this->user); } /** * Presence lists citizens in the city. */ public function test_presence_returns_citizens(): void { $this->character->forceFill(['is_online' => true, 'pos_x' => 5, 'pos_z' => 5])->save(); $this->getJson('/city/presence') ->assertOk() ->assertJsonStructure(['city_id', 'server_time', 'citizens' => [['id', 'name', 'position', 'is_me']]]); } /** * Market sell lists inventory and buy transfers money. */ public function test_market_sell_and_buy(): void { InventoryItem::updateOrCreate( ['character_id' => $this->character->id, 'item' => 'product'], ['qty' => 3], ); $this->postJson('/city/market/sell', [ 'item' => 'product', 'qty' => 2, 'unit_price' => 5, ])->assertOk(); $listing = MarketListing::firstOrFail(); $this->assertSame(1, (int) InventoryItem::where('character_id', $this->character->id)->where('item', 'product')->value('qty')); $buyerUser = User::factory()->create(['status' => User::STATUS_ACTIVE, 'is_active' => true]); $buyer = Character::ensureForUser($buyerUser); $buyer->forceFill(['money' => 50])->save(); $buy = $this->actingAs($buyerUser) ->postJson('/city/market/buy', ['listing_id' => $listing->id, 'qty' => 2]); $buy->assertOk(); $this->assertEqualsWithDelta(40.0, (float) $buy->json('money'), 0.01); $this->assertFalse($listing->fresh()->is_active); $this->assertSame(1, Transaction::count()); } /** * Completing exercise metric finishes the exercise mission with reward. */ public function test_mission_completes_on_exercise(): void { $mission = Mission::where('code', 'exercise-5')->firstOrFail(); $eq = CityPlazas::forCity($this->character->city)[0]['equipment'][0]; $this->character->forceFill([ 'pos_x' => $eq['position'][0], 'pos_z' => $eq['position'][1], 'health' => 50, 'energy' => 90, 'exercise_count' => 4, 'money' => 10, ])->save(); $this->postJson('/city/actions/exercise', [ 'pos_x' => $eq['position'][0], 'pos_z' => $eq['position'][1], ])->assertOk() ->assertJsonPath('character.exercise_count', 5); $pivot = DB::table('character_missions') ->where('character_id', $this->character->id) ->where('mission_id', $mission->id) ->first(); $this->assertNotNull($pivot); $this->assertTrue((bool) $pivot->is_completed); $this->assertSame(10.0 + 25.0, round((float) $this->character->fresh()->money, 2)); } /** * Appearance customization persists safe slots. */ public function test_customize_updates_appearance(): void { $this->putJson('/city/customize', [ 'skin' => 2, 'hair' => 3, 'shirt' => 4, 'face' => 1, 'beard' => 0, 'accessory' => 2, ])->assertOk() ->assertJsonPath('appearance.skin', 2); $this->character->refresh(); $this->assertSame(3, (int) $this->character->hair); } /** * Boarding a bus requires being near a road. */ public function test_ride_bus_requires_road(): void { $this->postJson('/city/actions/ride', [ 'kind' => 'bus', 'pos_x' => 10, 'pos_z' => 10, ])->assertStatus(422); $this->postJson('/city/actions/ride', [ 'kind' => 'bus', 'pos_x' => 1, 'pos_z' => 0, ])->assertOk() ->assertJsonPath('character.ride_kind', 'bus'); } /** * Admin world dashboard metrics endpoint. */ public function test_admin_world_metrics(): void { $admin = User::factory()->admin()->create(); foreach (LegalDocument::where('is_current', true)->pluck('id') as $docId) { UserConsent::create([ 'user_id' => $admin->id, 'legal_document_id' => $docId, 'accepted_at' => now(), 'ip_address' => '127.0.0.1', 'user_agent' => 'phpunit', 'source' => 'web_form', ]); } $this->actingAs($admin) ->get('/admin/world') ->assertOk() ->assertSee(__('admin.world.title'), false); } }