commit21
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Budget;
|
||||
use App\Models\Department;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\User;
|
||||
use App\Services\OrganizationAccess;
|
||||
@@ -26,6 +28,70 @@ beforeEach(function () {
|
||||
$this->actingAs($this->actor);
|
||||
});
|
||||
|
||||
test('requisition audit export includes every metadata record without private payloads or read side effects', function () {
|
||||
$this->postJson('/requisicoes', $this->data)->assertRedirect();
|
||||
$r = Requisition::query()->latest('id')->firstOrFail();
|
||||
for ($i = 0; $i < 105; $i++) {
|
||||
AuditLog::query()->create(['tenant_id' => $r->tenant_id, 'requisition_id' => $r->id, 'user_id' => $this->actor->id,
|
||||
'action' => $i === 0 ? "\t=FORMULA-TEST" : 'qa.metadata.'.$i, 'created_at' => now(),
|
||||
'before' => ['bank' => 'PRIVATE-BEFORE-MARKER'], 'after' => ['account_identifier' => 'PRIVATE-AFTER-MARKER', 'execution' => ['mode' => 'scheduled_replenishment']]]);
|
||||
}
|
||||
$expectedIds = AuditLog::query()->where('requisition_id', $r->id)->orderBy('id')->pluck('id')->all();
|
||||
$before = [];
|
||||
foreach (['requisitions', 'approval_steps', 'budget_entries', 'stock_movements', 'audit_logs'] as $table) {
|
||||
$before[$table] = DB::table($table)->orderBy('id')->get()->toArray();
|
||||
}
|
||||
$response = $this->get('/requisicoes/'.$r->id.'/historico.csv')->assertOk()->assertHeader('Content-Type', 'text/csv; charset=UTF-8')
|
||||
->assertHeader('Cache-Control', 'max-age=0, no-store, private');
|
||||
$csv = $response->streamedContent();
|
||||
expect($csv)->not->toContain('PRIVATE-BEFORE-MARKER')->not->toContain('PRIVATE-AFTER-MARKER')->toContain('scheduled_replenishment');
|
||||
$stream = fopen('php://temp', 'w+');
|
||||
fwrite($stream, $csv);
|
||||
rewind($stream);
|
||||
fgetcsv($stream, null, ';', '"', '');
|
||||
fgetcsv($stream, null, ';', '"', '');
|
||||
$ids = [];
|
||||
$actions = [];
|
||||
while (($row = fgetcsv($stream, null, ';', '"', '')) !== false) {
|
||||
$ids[] = (int) $row[0];
|
||||
$actions[] = $row[2];
|
||||
expect($row)->toHaveCount(5);
|
||||
}
|
||||
fclose($stream);
|
||||
expect($ids)->toBe($expectedIds)->and($actions[1])->toStartWith("'");
|
||||
$this->get('/requisicoes/'.$r->id)->assertInertia(fn (AssertableInertia $p) => $p
|
||||
->has('audit', 25)->where('auditPagination.total', count($expectedIds))->missing('audit.1.after')->where('audit.1.execution', 'scheduled_replenishment'));
|
||||
$this->get('/requisicoes/'.$r->id.'?audit_page=2')->assertInertia(fn (AssertableInertia $p) => $p
|
||||
->has('audit', 25)->where('audit.0.id', $expectedIds[25])->where('auditPagination.current_page', 2));
|
||||
$this->get('/requisicoes/'.$r->id.'?audit_page=5')->assertInertia(fn (AssertableInertia $p) => $p
|
||||
->has('audit', 6)->where('audit.0.id', $expectedIds[100])->where('auditPagination.next_page_url', null));
|
||||
$this->getJson('/requisicoes/'.$r->id.'?audit_page=0')->assertUnprocessable();
|
||||
$this->getJson('/requisicoes/'.$r->id.'?audit_page=1000001')->assertUnprocessable();
|
||||
foreach ($before as $table => $rows) {
|
||||
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows);
|
||||
}
|
||||
});
|
||||
|
||||
test('requisition audit export preserves requester department company and current access boundaries', function () {
|
||||
$this->postJson('/requisicoes', $this->data)->assertRedirect();
|
||||
$r = Requisition::query()->latest('id')->firstOrFail();
|
||||
$url = '/requisicoes/'.$r->id.'/historico.csv';
|
||||
$other = User::factory()->create(['tenant_id' => $r->tenant_id, 'department_id' => $r->department_id, 'purchasing_role' => PurchasingRole::Requester]);
|
||||
$this->actingAs($other)->get($url)->assertNotFound();
|
||||
$department = Department::query()->create(['tenant_id' => $r->tenant_id, 'name' => 'Outro departamento sintético', 'code' => 'OTHER-AUDIT']);
|
||||
$head = User::factory()->create(['tenant_id' => $r->tenant_id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::DepartmentHead]);
|
||||
$this->actingAs($head)->get($url)->assertNotFound();
|
||||
$foreign = receiptFixture();
|
||||
$this->actingAs($foreign['issuer'])->get($url)->assertNotFound();
|
||||
$this->actingAs($this->actor);
|
||||
require_once __DIR__.'/../Support/SupplyFixture.php';
|
||||
supplyGrant($this->actor, ['requester']);
|
||||
DB::table('organization_memberships')->where('user_id', $this->actor->id)->update(['maintenance' => true]);
|
||||
$this->get($url)->assertForbidden();
|
||||
DB::table('organization_memberships')->where('user_id', $this->actor->id)->update(['maintenance' => false, 'expires_at' => now()->subMinute()]);
|
||||
$this->get($url)->assertForbidden();
|
||||
});
|
||||
|
||||
test('autonomous adapter executes shared lifecycle with one request one reserve and independent versioned approval', function () {
|
||||
$count = Requisition::query()->count();
|
||||
$this->postJson('/requisicoes', $this->data)->assertRedirect();
|
||||
@@ -128,6 +194,57 @@ test('profile task catalogue covers the existing permissions without creating a
|
||||
->where('workflowTasks.department_head.0.key', 'requisition.approve.department_head'));
|
||||
});
|
||||
|
||||
test('notice consultation searches the full visible scope and reading preserves filters without deciding', function () {
|
||||
$requests = [];
|
||||
for ($i = 0; $i < 28; $i++) {
|
||||
$this->postJson('/requisicoes', [...$this->data, 'operation_key' => (string) Str::uuid()])->assertRedirect();
|
||||
$r = Requisition::query()->latest('id')->firstOrFail();
|
||||
$this->postJson('/requisicoes/'.$r->id.'/submeter', ['operation_key' => (string) Str::uuid(), 'process_version' => 1])->assertRedirect();
|
||||
$requests[] = $r->fresh();
|
||||
}
|
||||
$old = $requests[0];
|
||||
$notice = DB::table('workflow_notices')->where('requisition_id', $old->id)->sole();
|
||||
$before = [];
|
||||
foreach (['requisitions', 'approval_steps', 'budget_entries', 'stock_movements', 'workflow_notices', 'audit_logs'] as $table) {
|
||||
$before[$table] = DB::table($table)->orderBy('id')->get()->toArray();
|
||||
}
|
||||
$this->actingAs($this->head)->get('/requisicoes/avisos?view=tasks&search=req-&page=2')->assertOk()
|
||||
->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (AssertableInertia $p) => $p
|
||||
->where('notices.total', 28)->has('notices.data', 3)->where('unread', 28)
|
||||
->where('notices.prev_page_url', fn ($url) => str_contains($url, 'search=req-') && str_contains($url, 'view=tasks')));
|
||||
$path = '/requisicoes/avisos?view=unread&search='.urlencode(mb_strtolower($old->number)).'&page=999';
|
||||
$this->get($path)->assertInertia(fn (AssertableInertia $p) => $p->where('notices.total', 1)->where('notices.current_page', 1)
|
||||
->where('notices.data.0.id', $notice->id)->where('unread', 28));
|
||||
$this->get('/requisicoes/avisos?search='.urlencode("' OR 1=1 --"))->assertInertia(fn (AssertableInertia $p) => $p->where('notices.total', 0)->where('unread', 28));
|
||||
foreach ($before as $table => $rows) {
|
||||
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows);
|
||||
}
|
||||
$filters = ['view' => 'unread', 'page' => 2, 'search' => $old->number];
|
||||
$redirect = '/requisicoes/avisos?'.http_build_query($filters);
|
||||
$this->post('/requisicoes/avisos/'.$notice->id.'/ler', $filters)->assertRedirect($redirect);
|
||||
$this->post('/requisicoes/avisos/'.$notice->id.'/ler', $filters)->assertRedirect($redirect);
|
||||
$this->get($redirect)->assertInertia(fn (AssertableInertia $p) => $p->where('notices.total', 0)->where('notices.current_page', 1)->where('unread', 27));
|
||||
foreach (['requisitions', 'approval_steps', 'budget_entries', 'stock_movements'] as $table) {
|
||||
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($before[$table]);
|
||||
}
|
||||
expect(DB::table('audit_logs')->where('action', 'workflow.notice_read')->count())->toBe(1);
|
||||
$this->actingAs($this->actor)->get($path)->assertInertia(fn (AssertableInertia $p) => $p->where('notices.total', 0));
|
||||
});
|
||||
|
||||
test('notice consultation rejects invalid context before recording a read', function () {
|
||||
$this->postJson('/requisicoes', $this->data)->assertRedirect();
|
||||
$r = Requisition::query()->latest('id')->firstOrFail();
|
||||
$this->postJson('/requisicoes/'.$r->id.'/submeter', ['operation_key' => (string) Str::uuid(), 'process_version' => 1])->assertRedirect();
|
||||
$notice = DB::table('workflow_notices')->where('requisition_id', $r->id)->sole();
|
||||
$this->actingAs($this->head);
|
||||
foreach ([['search' => str_repeat('A', 101)], ['page' => 0], ['view' => 'invalid']] as $filters) {
|
||||
$this->getJson('/requisicoes/avisos?'.http_build_query($filters))->assertUnprocessable();
|
||||
$this->postJson('/requisicoes/avisos/'.$notice->id.'/ler', $filters)->assertUnprocessable();
|
||||
}
|
||||
expect(DB::table('workflow_notices')->where('id', $notice->id)->value('read_at'))->toBeNull();
|
||||
expect(DB::table('audit_logs')->where('action', 'workflow.notice_read')->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('internal notices are created once with submission and advance only after a valid decision', function () {
|
||||
$this->f['tenant']->update(['first_limit_minor' => 1, 'second_limit_minor' => 2]);
|
||||
$buyer = User::factory()->create(['tenant_id' => $this->actor->tenant_id, 'department_id' => $this->actor->department_id, 'purchasing_role' => PurchasingRole::Procurement]);
|
||||
|
||||
Reference in New Issue
Block a user