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.
|
- 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.
|
- 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
|
## Definição de concluído
|
||||||
|
|
||||||
Uma alteração só está concluída quando:
|
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.
|
- 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.
|
- 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.
|
- 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
|
### 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.
|
- 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.
|
- 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.
|
- 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 App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,12 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
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) => [
|
$roles = collect(RoleName::cases())->mapWithKeys(fn (RoleName $role) => [
|
||||||
$role->value => Role::firstOrCreate(
|
$role->value => Role::firstOrCreate(
|
||||||
['name' => $role->value],
|
['name' => $role->value],
|
||||||
@@ -31,25 +38,38 @@ class DatabaseSeeder extends Seeder
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$demo = User::factory()->create([
|
$demo = User::query()->updateOrCreate(
|
||||||
'name' => 'Utilizador de Demonstração',
|
['email' => 'demo@escola.pt'],
|
||||||
'email' => 'demo@escola.pt',
|
[
|
||||||
'department' => 'Docentes',
|
'name' => 'Utilizador de Demonstração',
|
||||||
]);
|
'email_verified_at' => now(),
|
||||||
$demo->roles()->attach($roles[RoleName::Requester->value]);
|
'password' => Hash::make('password'),
|
||||||
|
'department' => 'Docentes',
|
||||||
|
'active' => true,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
$demo->roles()->sync([$roles[RoleName::Requester->value]->id]);
|
||||||
|
|
||||||
$admin = User::factory()->create([
|
$admin = User::query()->updateOrCreate(
|
||||||
'name' => 'Administrador do Portal',
|
['email' => 'admin@escola.pt'],
|
||||||
'email' => 'admin@escola.pt',
|
[
|
||||||
'department' => 'Serviços de Informática',
|
'name' => 'Administrador do Portal',
|
||||||
]);
|
'email_verified_at' => now(),
|
||||||
$admin->roles()->attach([
|
'password' => Hash::make('password'),
|
||||||
|
'department' => 'Serviços de Informática',
|
||||||
|
'active' => true,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
$admin->roles()->sync([
|
||||||
$roles[RoleName::Requester->value]->id,
|
$roles[RoleName::Requester->value]->id,
|
||||||
$roles[RoleName::Approver->value]->id,
|
$roles[RoleName::Approver->value]->id,
|
||||||
$roles[RoleName::InventoryOperator->value]->id,
|
$roles[RoleName::InventoryOperator->value]->id,
|
||||||
$roles[RoleName::Administrator->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;
|
- `docker compose exec -T app composer test`: passou;
|
||||||
- Laravel Pint: 141 ficheiros sem desvios;
|
- Laravel Pint: 141 ficheiros sem desvios;
|
||||||
- PHPStan/Larastan nível 7: 107 ficheiros, zero erros;
|
- 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;
|
- TypeScript (`tsc --noEmit`): passou;
|
||||||
- ESLint: passou;
|
- ESLint: passou;
|
||||||
- Prettier: 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`;
|
- 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;
|
- 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`.
|
- 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.
|
- 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;
|
- 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;
|
- 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;
|
- 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;
|
- 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.
|
- 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
|
## 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());"
|
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
|
```bash
|
||||||
docker compose exec app php artisan db:seed --force
|
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
|
## 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