This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
38
tests/Feature/DatabaseSeederTest.php
Normal file
38
tests/Feature/DatabaseSeederTest.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Identity\Enums\RoleName;
|
||||
use App\Domain\Inventory\Models\SnipeAsset;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\DatabaseSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('repõe identidades de desenvolvimento sem duplicar dados', function () {
|
||||
config()->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();
|
||||
});
|
||||
Reference in New Issue
Block a user