Preserva entrega A aceite de servicos em rascunho
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$fixture = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$base = ['actor' => $fixture['manager_id'], 'tenant' => $fixture['tenant'], 'company' => $fixture['company'], 'unit' => $fixture['unit']];
|
||||
function pair(array $firstInput, array $secondInput): array {
|
||||
$first = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($firstInput + ['hold' => true]));
|
||||
$second = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($secondInput));
|
||||
$first->start();
|
||||
if (! $first->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'LOCK_ACQUIRED'))) { throw new RuntimeException('Could not establish concurrent lock test.'); }
|
||||
$second->start(); $first->wait(); $second->wait();
|
||||
$results = [];
|
||||
foreach ([$first, $second] as $process) {
|
||||
if (! $process->isSuccessful()) { throw new RuntimeException('Synthetic concurrency process failed.'); }
|
||||
$lines = array_values(array_filter(explode("\n", trim($process->getOutput())), fn ($line) => str_starts_with($line, '{')));
|
||||
$results[] = json_decode(end($lines), true, 32, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
$beforeEvents = DB::table('a_events')->count();
|
||||
$key = (string) \Illuminate\Support\Str::uuid();
|
||||
$input = $base + ['data' => ['name' => 'Cliente sintético de concorrência'], 'key' => $key];
|
||||
[$first, $second] = pair($input, $input);
|
||||
if ($first['status'] !== 200 || $second['status'] !== 200 || $first['result']['id'] !== $second['result']['id'] || ! $second['result']['replayed'] || DB::table('a_events')->count() !== $beforeEvents + 1) { throw new RuntimeException('Concurrent repetition was not deduplicated.'); }
|
||||
$id = $first['result']['id'];
|
||||
[$edit, $conflict] = pair($base + ['id' => $id, 'data' => ['name' => 'Versão concorrente preservada', 'version' => 1], 'key' => (string) \Illuminate\Support\Str::uuid()],
|
||||
$base + ['id' => $id, 'data' => ['name' => 'Escrita antiga recusada', 'version' => 1], 'key' => (string) \Illuminate\Support\Str::uuid()]);
|
||||
if ($edit['status'] !== 200 || $conflict['status'] !== 409 || DB::table('a_events')->count() !== $beforeEvents + 2 || DB::table('a_clients')->where('id', $id)->value('name') !== 'Versão concorrente preservada') { throw new RuntimeException('Concurrent version conflict was not protected.'); }
|
||||
$report = ['environment' => 'sgf_tempo_test only', 'parallel_php_processes' => true, 'company_lock_overlap' => true,
|
||||
'identical_create' => ['same_record' => true, 'second_replayed' => true, 'events_created' => 1],
|
||||
'concurrent_edit' => ['first_status' => 200, 'second_status' => 409, 'version_after' => 2, 'events_created' => 1]];
|
||||
file_put_contents(base_path('docs/proofs/a-concurrency.json'), json_encode($report, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Concurrent PHP/PostgreSQL proof passed: one repeated creation, one version-conflicting edit, no duplicated events.\n";
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$input = json_decode(stream_get_contents(STDIN), true, 32, JSON_THROW_ON_ERROR);
|
||||
$context = app(\App\Shared\Context\CompanyContext::class);
|
||||
$context->selectFor($input['actor'], $input['tenant'], $input['company'], $input['unit']);
|
||||
try {
|
||||
$result = \Illuminate\Support\Facades\DB::transaction(function () use ($input) {
|
||||
if ($input['hold'] ?? false) {
|
||||
\Illuminate\Support\Facades\DB::table('companies')->where('id', $input['company'])->lockForUpdate()->firstOrFail();
|
||||
echo "LOCK_ACQUIRED\n"; flush(); usleep(2000000);
|
||||
}
|
||||
return app(\App\Modules\Services\ServiceWorkflow::class)->save('clients', $input['id'] ?? null, $input['actor'], $input['data'], $input['key']);
|
||||
});
|
||||
echo json_encode(['status' => 200, 'result' => $result], JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception) {
|
||||
echo json_encode(['status' => $exception->getStatusCode()], JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
} catch (\Throwable) { fwrite(STDERR, "Synthetic concurrency worker failed; no request payload displayed.\n"); exit(1); }
|
||||
@@ -0,0 +1,50 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const fixture = JSON.parse(await fs.readFile('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json','utf8'));
|
||||
const base = 'http://127.0.0.1:8001';
|
||||
function client() {
|
||||
const cookies = new Map(); let csrf = ''; let context = '';
|
||||
return {
|
||||
cookies,
|
||||
async request(path, method='GET', body, {key=randomUUID(), token=context, useCsrf=true}={}) {
|
||||
const headers={Accept:'application/json','Content-Type':'application/json', Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),'X-SGF-Context':token,'Idempotency-Key':key};
|
||||
if(useCsrf) headers['X-CSRF-TOKEN']=csrf;
|
||||
const response=await fetch(base+path,{method,headers,...(body===undefined?{}:{body:JSON.stringify(body)})});
|
||||
for(const cookie of response.headers.getSetCookie()){const pair=cookie.split(';',1)[0], split=pair.indexOf('=');cookies.set(pair.slice(0,split),pair.slice(split+1));}
|
||||
const data=await response.json().catch(()=>({}));
|
||||
if(data.csrf)csrf=data.csrf;if(data.context?.token)context=data.context.token;
|
||||
return {status:response.status,data};
|
||||
},
|
||||
async login(email){await this.request('/api/session');assert.equal((await this.request('/api/login','POST',{email,password:fixture.password})).status,200);},
|
||||
async select(unit=fixture.unit){const result=await this.request('/api/context','POST',{tenant_id:fixture.tenant,company_id:fixture.company,unit_id:unit});assert.equal(result.status,200);return result.data.context.token;}
|
||||
};
|
||||
}
|
||||
const manager=client(),reader=client(); await manager.login('gestor@example.test');const oldToken=await manager.select();await reader.login('consulta@example.test');await reader.select();
|
||||
const results=[];
|
||||
const key=randomUUID();const created=await manager.request('/api/a/clients','POST',{name:'Cliente sintético HTTP A'},{key});assert.equal(created.status,201);
|
||||
const replay=await manager.request('/api/a/clients','POST',{name:'Cliente sintético HTTP A'},{key});assert.equal(replay.status,201);assert.equal(replay.data.id,created.data.id);assert.equal(replay.data.event_id,created.data.event_id);assert.equal(replay.data.replayed,true);
|
||||
assert.equal((await manager.request('/api/a/clients','POST',{name:'Conteúdo alterado'},{key})).status,409);
|
||||
results.push('Criação repetida recupera o mesmo registo/evento; conteúdo diferente com a mesma chave é recusado (409).');
|
||||
const path=`/api/a/clients/${created.data.id}`;
|
||||
assert.equal((await reader.request(path)).status,200);
|
||||
assert.equal((await reader.request(path,'PATCH',{name:'Não autorizado',version:1})).status,403);
|
||||
assert.equal((await reader.request('/api/a/clients/access')).status,403);
|
||||
results.push('Consulta por credenciais reais sintéticas: leitura 200, edição e gestão 403.');
|
||||
const grant={email:'consulta@example.test',action:'edit',grant:true,reason:'Prova HTTP A, exclusivamente sintética'};
|
||||
assert.equal((await manager.request('/api/a/clients/access','POST',grant)).status,200);
|
||||
assert.equal((await reader.request(path,'PATCH',{name:'Edição sintética autorizada',version:1})).status,200);
|
||||
assert.equal((await manager.request('/api/a/clients/access','POST',{...grant,grant:false})).status,200);
|
||||
assert.equal((await reader.request(path,'PATCH',{name:'Direito revogado',version:2})).status,403);
|
||||
results.push('Concessão/revogação de edição têm efeito no pedido seguinte, mantendo a mesma sessão.');
|
||||
assert.equal((await manager.request(path,'PATCH',{name:'Versão antiga',version:1})).status,409);
|
||||
await manager.select(null);
|
||||
assert.equal((await manager.request(path,'PATCH',{name:'Contexto antigo',version:2},{token:oldToken})).status,409);
|
||||
assert.equal((await manager.request('/api/a/clients','POST',{name:'Sem CSRF'},{useCsrf:false})).status,419);
|
||||
results.push('Versão antiga e contexto antigo recusados (409); alteração sem protecção CSRF/origem recusada (419).');
|
||||
const previousCookie=new Map(reader.cookies);assert.equal((await reader.request('/api/logout','POST')).status,200);reader.cookies.clear();for(const [k,v] of previousCookie)reader.cookies.set(k,v);
|
||||
assert.equal((await reader.request('/api/a/clients')).status,401);await manager.request('/api/logout','POST');
|
||||
results.push('Cookie anterior à saída não recupera acesso aos dados de A (401).');
|
||||
await fs.writeFile(new URL('../docs/proofs/a-http-results.json',import.meta.url),JSON.stringify({environment:'browser-testing / sgf_tempo_test only',results},null,2));
|
||||
console.log(results.join('\n'));
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$counts = [];
|
||||
foreach (['a_clients','a_services','a_service_drafts','a_billing_drafts','a_history','a_events','a_outbox','a_pending','a_commands'] as $table) { $counts[$table] = \Illuminate\Support\Facades\DB::table($table)->count(); }
|
||||
$missing = \Illuminate\Support\Facades\DB::table('a_events as e')->leftJoin('a_outbox as o', 'o.event_id', '=', 'e.id')->whereNull('o.event_id')->count();
|
||||
if ($missing !== 0 || $counts['a_history'] !== $counts['a_events'] || $counts['a_events'] !== $counts['a_outbox']) { throw new RuntimeException('Incomplete durable capture.'); }
|
||||
$report = ['database' => 'sgf_tempo_test', 'counts' => $counts, 'events_without_outbox' => $missing, 'outbox_states' => \Illuminate\Support\Facades\DB::table('a_outbox')->distinct()->pluck('state')->all()];
|
||||
file_put_contents(base_path('docs/proofs/a-durable-capture.json'), json_encode($report, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode($report, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
if (! \App\Modules\Identity\Services\LocalDatabaseGuard::allows()) { exit(1); }
|
||||
$counts = [];
|
||||
foreach (['a_clients','a_services','a_service_drafts','a_billing_drafts','a_history','a_events','a_outbox','a_pending','a_commands'] as $table) { $counts[$table] = \Illuminate\Support\Facades\DB::table($table)->count(); }
|
||||
$permissions = array_merge(\App\Modules\Services\ServicePermissions::catalogue(false), \App\Modules\Services\ServicePermissions::catalogue(true));
|
||||
$grants = \Illuminate\Support\Facades\DB::table('permission_grants as g')->join('users as u', 'u.id', '=', 'g.user_id')->whereIn('g.permission', $permissions)->orderBy('u.id')->orderBy('g.permission')->get(['u.id as user_id','u.name','g.tenant_id','g.company_id','g.unit_id','g.permission']);
|
||||
echo json_encode(['database' => 'sgf_tempo_dev', 'role' => 'sgf_dev_app', 'records' => $counts, 'explicit_grants' => $grants], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;
|
||||
@@ -9,7 +9,9 @@ foreach (['users', 'tenants', 'companies', 'units'] as $table) {
|
||||
fwrite(STDERR, "Test database is not empty. No data was deleted. Stop other test workflows and prepare the test database explicitly.\n"); exit(1);
|
||||
}
|
||||
}
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\IdentityFixture::create());
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\ServicesFixture::create());
|
||||
$safe = array_intersect_key($fixture, array_flip(['tenant', 'otherTenant', 'company', 'sibling', 'foreign', 'unit', 'hidden', 'siblingUnit', 'password']));
|
||||
$safe['manager_id'] = $fixture['manager']->id;
|
||||
$safe['reader_id'] = $fixture['reader']->id;
|
||||
file_put_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json', json_encode($safe, JSON_THROW_ON_ERROR));
|
||||
echo "Synthetic browser fixtures prepared only in sgf_tempo_test. Credentials stored locally with restricted ACL; not displayed.\n";
|
||||
|
||||
Reference in New Issue
Block a user