feat(world): add persistent citizen simulation with needs, jobs, and social features
- Adds Character model with survival needs (hunger, thirst, energy) and state machine - Implements city:tick world ticker for offline progression every minute - Adds city territory expansion and building construction system - Introduces job economy, inventory, and transaction audit trail - Adds social graph (friends, marriage, children) and character chat - Implements admin approval workflow for new registrations - Adds hospital admission system for traffic accidents - Includes comprehensive feature tests and world seeders
This commit is contained in:
Executable
+167
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Character;
|
||||
use App\Models\City;
|
||||
use App\Models\LegalDocument;
|
||||
use App\Models\User;
|
||||
use App\Models\UserConsent;
|
||||
use Database\Seeders\CityWorldSeeder;
|
||||
use Database\Seeders\LegalDocumentSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Registration approval workflow for citizen accounts.
|
||||
*/
|
||||
class CitizenApprovalTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Boot legal docs + world for each test.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(LegalDocumentSeeder::class);
|
||||
$this->seed(CityWorldSeeder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an admin that already accepted all current legal documents.
|
||||
*/
|
||||
private function makeAdmin(): User
|
||||
{
|
||||
$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',
|
||||
]);
|
||||
}
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* New registrations must start as pending, not active.
|
||||
*/
|
||||
public function test_register_creates_pending_user(): void
|
||||
{
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Nova Pessoa',
|
||||
'email' => 'nova@example.com',
|
||||
'password' => 'Secret123!',
|
||||
'password_confirmation' => 'Secret123!',
|
||||
'locale' => 'pt_BR',
|
||||
'captcha' => $this->captchaForRegister(),
|
||||
]);
|
||||
|
||||
$user = User::where('email', 'nova@example.com')->first();
|
||||
$this->assertNotNull($user);
|
||||
$this->assertSame(User::STATUS_PENDING, $user->status);
|
||||
$this->assertFalse($user->is_active);
|
||||
$response->assertRedirect(route('terms.accept'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending users are sent to the approval wait screen from the dashboard.
|
||||
*/
|
||||
public function test_pending_user_sees_wait_screen(): void
|
||||
{
|
||||
$user = User::factory()->pending()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/dashboard')
|
||||
->assertRedirect(route('account.pending'));
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('account.pending'))
|
||||
->assertOk()
|
||||
->assertSee(__('auth.pending_title'), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login attempt by a pending account shows the approval message.
|
||||
*/
|
||||
public function test_login_rejects_pending_with_message(): void
|
||||
{
|
||||
$user = User::factory()->pending()->create(['password' => 'Secret123!']);
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'Secret123!',
|
||||
'captcha' => $this->captchaForLogin(),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
$this->assertSame(__('auth.pending'), session('errors')->first('email'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin approval activates the account and creates the character.
|
||||
*/
|
||||
public function test_admin_approval_creates_character(): void
|
||||
{
|
||||
$admin = $this->makeAdmin();
|
||||
$pending = User::factory()->pending()->create(['name' => 'Ana Silva']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post(route('admin.citizens.approve', $pending))
|
||||
->assertRedirect(route('admin.citizens.index'));
|
||||
|
||||
$pending->refresh();
|
||||
$this->assertSame(User::STATUS_ACTIVE, $pending->status);
|
||||
$this->assertTrue($pending->is_active);
|
||||
|
||||
$character = Character::query()->where('user_id', $pending->id)->first();
|
||||
$this->assertNotNull($character);
|
||||
$this->assertSame('Ana', $character->name);
|
||||
$this->assertNotNull(City::active());
|
||||
$this->assertSame(City::active()->id, $character->city_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Banned accounts cannot authenticate.
|
||||
*/
|
||||
public function test_banned_user_cannot_login(): void
|
||||
{
|
||||
$user = User::factory()->inactive()->create(['password' => 'Secret123!']);
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'Secret123!',
|
||||
'captcha' => $this->captchaForLogin(),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Solve the session math CAPTCHA for a register request.
|
||||
*/
|
||||
private function captchaForRegister(): int
|
||||
{
|
||||
// Prime the session by loading the form first in real usage; in tests
|
||||
// we set the session keys directly to avoid an extra round-trip.
|
||||
$a = random_int(1, 9);
|
||||
$b = random_int(1, 9);
|
||||
session(['captcha_result' => $a + $b, 'captcha_expires_at' => now()->addMinutes(10)->getTimestamp()]);
|
||||
|
||||
return $a + $b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solve the session math CAPTCHA for a login request.
|
||||
*/
|
||||
private function captchaForLogin(): int
|
||||
{
|
||||
return $this->captchaForRegister();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user