diff --git a/AGENTS.md b/AGENTS.md index 8b80b36..ec2e776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,11 @@ Estas instruções aplicam-se a todo o repositório. - Escritas de checkout/check-in nunca têm retry automático. Um timeout ou resultado ambíguo interrompe o fluxo até validação manual no Snipe-IT. - Nunca revelar URL privada, tokens, payloads sensíveis ou respostas completas de produção em logs, documentação, testes ou mensagens. +## Isolamento de testes e dados locais + +- Nunca executar `php artisan test` diretamente com a configuração local: pode aplicar `RefreshDatabase` à base PostgreSQL de desenvolvimento. Usar sempre `docker compose exec -T app composer test`, que força SQLite em memória, ou declarar explicitamente o mesmo ambiente isolado. +- O `DatabaseSeeder` contém credenciais conhecidas e só pode atuar em `local`/`testing`. Quando o Snipe-IT está configurado ou já existe cache de inventário, não pode criar ativos fictícios. + ## Definição de concluído Uma alteração só está concluída quando: diff --git a/CHANGELOG.md b/CHANGELOG.md index 03b5c88..b9a3c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Todas as alterações materiais são registadas neste ficheiro. O formato segue - Notificações passam a obter assunto e mensagem dos modelos administrativos, preservando defaults seguros. - Checkout Snipe-IT passa a consultar o estado remoto imediatamente antes do movimento e recusa ativos já atribuídos sem enviar escrita. - Check-in Snipe-IT passa a exigir checkout comprovado pelo portal para o mesmo requisitante e atribuição remota atual ao destinatário esperado. +- Seeder de desenvolvimento passa a ser idempotente, limitado a `local`/`testing` e a não criar ativos fictícios quando existe configuração ou cache Snipe-IT. ### Segurança @@ -93,3 +94,5 @@ Todas as alterações materiais são registadas neste ficheiro. O formato segue - Suite backend após as salvaguardas de produção: 108 testes, 609 asserções, Pint e PHPStan sem erros. - Tokens Snipe-IT distintos validados apenas com `GET`; sincronização real de leitura processou 2367 ativos para o cache local sem escrita remota. - TypeScript, ESLint, Prettier, build Vite, Composer Audit, migrations, liveness, readiness e login HTTP validados novamente. +- Recuperação do login administrativo validada de ponta a ponta no browser; 2 utilizadores e 5 perfis repostos sem alterar os 2367 ativos sincronizados. +- Suite após o hardening do seeder: 110 testes, 617 asserções, 142 ficheiros Pint e PHPStan sem erros. diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 9cbbe46..d6a11a6 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -8,6 +8,7 @@ use App\Domain\Inventory\Models\SnipeAsset; use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { @@ -18,6 +19,12 @@ class DatabaseSeeder extends Seeder */ public function run(): void { + if (! app()->environment(['local', 'testing'])) { + $this->command->warn('O DatabaseSeeder contém credenciais de desenvolvimento e foi ignorado fora de local/testing.'); + + return; + } + $roles = collect(RoleName::cases())->mapWithKeys(fn (RoleName $role) => [ $role->value => Role::firstOrCreate( ['name' => $role->value], @@ -31,25 +38,38 @@ class DatabaseSeeder extends Seeder ), ]); - $demo = User::factory()->create([ - 'name' => 'Utilizador de Demonstração', - 'email' => 'demo@escola.pt', - 'department' => 'Docentes', - ]); - $demo->roles()->attach($roles[RoleName::Requester->value]); + $demo = User::query()->updateOrCreate( + ['email' => 'demo@escola.pt'], + [ + 'name' => 'Utilizador de Demonstração', + 'email_verified_at' => now(), + 'password' => Hash::make('password'), + 'department' => 'Docentes', + 'active' => true, + ], + ); + $demo->roles()->sync([$roles[RoleName::Requester->value]->id]); - $admin = User::factory()->create([ - 'name' => 'Administrador do Portal', - 'email' => 'admin@escola.pt', - 'department' => 'Serviços de Informática', - ]); - $admin->roles()->attach([ + $admin = User::query()->updateOrCreate( + ['email' => 'admin@escola.pt'], + [ + 'name' => 'Administrador do Portal', + 'email_verified_at' => now(), + 'password' => Hash::make('password'), + 'department' => 'Serviços de Informática', + 'active' => true, + ], + ); + $admin->roles()->sync([ $roles[RoleName::Requester->value]->id, $roles[RoleName::Approver->value]->id, $roles[RoleName::InventoryOperator->value]->id, $roles[RoleName::Administrator->value]->id, ]); - SnipeAsset::factory()->count(18)->create(); + $snipeItConfigured = filled(config('services.snipe_it.url')) && filled(config('services.snipe_it.read_token')); + if (! $snipeItConfigured && ! SnipeAsset::query()->exists()) { + SnipeAsset::factory()->count(18)->create(); + } } } diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 42394d2..01cae64 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -89,7 +89,7 @@ Validação concluída em 5 de agosto de 2026: - `docker compose exec -T app composer test`: passou; - Laravel Pint: 141 ficheiros sem desvios; - PHPStan/Larastan nível 7: 107 ficheiros, zero erros; -- Pest: 108 testes passaram, 609 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada; +- Pest: 110 testes passaram, 617 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada; - TypeScript (`tsc --noEmit`): passou; - ESLint: passou; - Prettier: passou; @@ -110,7 +110,7 @@ Validação concluída em 5 de agosto de 2026: - backup PostgreSQL real em formato custom criado com checksum e validado por `pg_restore --list`; - restauro real numa base temporária: passou e confirmou 12 migrations antes da remoção automática; - smoke tests via Nginx: `/health/live` devolveu `ok` e `/health/ready` devolveu `ready`. -- diagnóstico de login local: a base de desenvolvimento estava vazia apesar de migrada; `db:seed --force` repôs 2 utilizadores ativos, 5 perfis e 18 ativos fictícios, sem substituir dados existentes; +- diagnóstico de login local anterior à ligação de produção: a base de desenvolvimento estava vazia apesar de migrada; `db:seed --force` repôs 2 utilizadores ativos, 5 perfis e, nesse contexto sem Snipe-IT configurado, 18 ativos fictícios; - login real no browser validado com `demo@escola.pt`: redirecionou para `/dashboard` e não produziu erros de consola. - testes focados das salvaguardas Snipe-IT: 17 passaram, com 106 asserções, incluindo prova de ausência de `POST` em ativos atribuídos e check-in sem proveniência ou destinatário válidos; - tokens Snipe-IT confirmados como configurados e distintos; ambos responderam a uma consulta `GET` com sucesso, sem revelar credenciais ou URL; @@ -118,6 +118,10 @@ Validação concluída em 5 de agosto de 2026: - cache local após sincronização: 2367 ativos ativos, 716 atribuídos e 144 marcados como requisitáveis; - frontend: TypeScript, ESLint, Prettier e build Vite passaram novamente; Composer Audit sem avisos conhecidos; - smoke tests após a sincronização: liveness `ok`, readiness `ready` e `/login` HTTP 200. +- nova recuperação do login após deteção de 0 utilizadores/0 perfis: seeder tornado idempotente e restrito a `local`/`testing`; repôs 2 utilizadores e 5 perfis sem alterar os 2367 ativos reais no cache; +- login administrativo validado de ponta a ponta no browser integrado: `admin@escola.pt` redirecionou para `/dashboard` e apresentou as áreas administrativas; +- testes específicos do seeder: reposição repetida sem duplicados e preservação do cache Snipe-IT configurado; suite total com 110 testes e 617 asserções; +- regra permanente adicionada: testes Laravel usam `composer test`/SQLite em memória e nunca `php artisan test` diretamente sobre a configuração PostgreSQL local. ## Decisões pendentes do contexto institucional diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 415aa4f..39f411b 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -54,13 +54,15 @@ Um `POST /login` com resposta 302 seguido novamente de `/login` indica normalmen docker compose exec app php artisan tinker --execute="dump(App\\Models\\User::count());" ``` -Se o resultado for zero e esta for a base local descartável, repor apenas os dados fictícios com: +Se o resultado for zero e esta for a base local descartável, repor as identidades de desenvolvimento com: ```bash docker compose exec app php artisan db:seed --force ``` -As contas são `demo@escola.pt` e `admin@escola.pt`, ambas com a palavra-passe de desenvolvimento `password`. Nunca executar o seeder para corrigir acessos em produção; aí deve ser usado o processo administrativo de contas/recuperação. +As contas são `demo@escola.pt` e `admin@escola.pt`, ambas com a palavra-passe de desenvolvimento `password`. O seeder é idempotente e só atua em `local`/`testing`. Se o Snipe-IT estiver configurado ou já existir inventário no cache, repõe contas/perfis sem criar ativos fictícios. Nunca executar o seeder para corrigir acessos em produção; aí deve ser usado o processo administrativo de contas/recuperação. + +Nunca executar `php artisan test` diretamente com a configuração local. Usar `docker compose exec -T app composer test`, que força SQLite em memória; testes com `RefreshDatabase` sobre a ligação PostgreSQL local podem deixar a base de desenvolvimento sem utilizadores. ## Atualização de código diff --git a/tests/Feature/DatabaseSeederTest.php b/tests/Feature/DatabaseSeederTest.php new file mode 100644 index 0000000..00fff14 --- /dev/null +++ b/tests/Feature/DatabaseSeederTest.php @@ -0,0 +1,38 @@ +set('services.snipe_it.url', null); + config()->set('services.snipe_it.read_token', null); + + $this->seed(DatabaseSeeder::class); + $this->seed(DatabaseSeeder::class); + + $admin = User::query()->where('email', 'admin@escola.pt')->firstOrFail(); + + expect(User::query()->count())->toBe(2) + ->and($admin->roles()->count())->toBe(4) + ->and($admin->hasRole(RoleName::Administrator))->toBeTrue() + ->and(Hash::check('password', $admin->password))->toBeTrue() + ->and(SnipeAsset::query()->count())->toBe(18); +}); + +it('não mistura ativos fictícios com o cache Snipe-IT configurado', function () { + config()->set('services.snipe_it.url', 'https://inventory.example.test'); + config()->set('services.snipe_it.read_token', 'read-token-for-tests'); + $realAsset = SnipeAsset::factory()->create(); + + $this->seed(DatabaseSeeder::class); + + expect(SnipeAsset::query()->count())->toBe(1) + ->and(SnipeAsset::query()->firstOrFail()->is($realAsset))->toBeTrue() + ->and(User::query()->where('email', 'admin@escola.pt')->exists())->toBeTrue(); +});