- Add database migrations for market_listings, missions and character cosmetics - Create controllers for marketplace, missions, presence and character customization - Implement admin world dashboard with live platform metrics - Add feature tests for world functionality - Include phase 2 design documents and gap analysis
178 lines
5.4 KiB
PHP
Executable File
178 lines
5.4 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Character;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\LegalDocument;
|
|
use App\Models\MarketListing;
|
|
use App\Models\Mission;
|
|
use App\Models\Transaction;
|
|
use App\Models\User;
|
|
use App\Models\UserConsent;
|
|
use App\Support\CityPlazas;
|
|
use Database\Seeders\CityWorldSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Presence, marketplace, missions, customize and ride endpoints.
|
|
*/
|
|
class WorldFeaturesTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $user;
|
|
|
|
protected Character $character;
|
|
|
|
/**
|
|
* Boot world fixtures.
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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);
|
|
}
|
|
}
|