This commit is contained in:
2026-11-02 08:06:45 +02:00
parent e4f520092e
commit 2a4169162b
108 changed files with 3793 additions and 88 deletions
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Support\IdentityFixture;
use Tests\TestCase;
// Transactions only: never migrate/fresh or reset the persistent demonstration.
#[\PHPUnit\Framework\Attributes\Group('modular-focused')]
final class ModuleNavigationTest extends TestCase
{
use DatabaseTransactions;
private array $f;
protected function setUp(): void
{
parent::setUp();
$this->f = IdentityFixture::create('-module-'.bin2hex(random_bytes(6)));
foreach (['clients.view','services.view'] as $p) IdentityFixture::grant($this->f['reader']->id,$this->f['tenant'],$this->f['company'],$p);
IdentityFixture::grant($this->f['reader']->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']);
$this->actingAs($this->f['reader'])->withSession(['auth_version'=>$this->f['reader']->auth_version]);
}
private function select(?string $module, ?int $company=null)
{
return $this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$company??$this->f['company'],'unit_id'=>$this->f['unit'],'module_id'=>$module]);
}
public function test_explicit_selection_direct_access_and_old_generation(): void
{
$this->select(null)->assertOk();
$this->getJson('/api/a/clients')->assertStatus(409);
$token=$this->select('commercial')->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertOk();
$this->getJson('/api/workshop')->assertStatus(409);
$next=$this->select('workshop')->assertOk()->json('context.token');
$this->assertNotSame($token,$next);
$this->withHeader('X-SGF-Context',$token)->getJson('/api/workshop')->assertStatus(409);
$this->withHeader('X-SGF-Context',$next)->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[]);
$this->postJson('/api/a/clients',['name'=>'Must not be inserted'])->assertForbidden();
$this->assertFalse(DB::table('a_clients')->where('name','Must not be inserted')->exists());
}
public function test_company_configuration_and_revocation_are_checked_again(): void
{
$token=$this->select('commercial')->assertOk()->json('context.token');
DB::table('permission_grants')->where('user_id',$this->f['reader']->id)->whereIn('permission',['clients.view','services.view'])->delete();
$this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertForbidden();
$this->getJson('/api/session')->assertOk()->assertJsonPath('context.module_id',null);
$this->select('commercial')->assertForbidden();
$this->select('workshop',$this->f['sibling'])->assertForbidden();
config(['modules.companies.'.$this->f['tenant'].':'.$this->f['company']=>['contracted'=>['workshop'],'enabled'=>[]]]);
$this->select('workshop')->assertForbidden();
}
public function test_development_discovery_never_grants_operations_or_applies_in_production(): void
{
config(['modules.contracted'=>[], 'modules.development_profiles'=>[$this->f['reader']->id.':'.$this->f['tenant'].':'.$this->f['company']]]);
$selected=$this->select('purchasing')->assertOk();
$this->assertCount(6,$selected->json('context.modules'));
$this->withHeader('X-SGF-Context',$selected->json('context.token'))->getJson('/api/supplier-payables')->assertForbidden();
app()->detectEnvironment(fn()=>'production');
$this->getJson('/api/session')->assertOk()->assertJsonPath('context.modules',[])->assertJsonPath('context.module_id',null);
}
public function test_failed_selection_preserves_context_and_audit_contains_module_without_token(): void
{
$selected=$this->select('commercial')->assertOk();
$this->select('invented')->assertForbidden();
$session=$this->getJson('/api/session')->assertOk();
$session->assertJsonPath('context.module_id','commercial')->assertJsonPath('context.token',$selected->json('context.token'));
$audit=DB::table('identity_audit')->where('actor_id',$this->f['reader']->id)->where('action','context.selected')->orderByDesc('id')->first();
$this->assertSame('commercial',json_decode($audit->details,true)['module_id']);
$this->assertStringNotContainsString($selected->json('context.token'),$audit->details);
}
public function test_local_environment_and_copied_profile_cannot_enable_discovery_for_client_database_configuration(): void
{
$this->select(null)->assertOk();
config(['modules.contracted'=>[], 'modules.development_profiles'=>[$this->f['reader']->id.':'.$this->f['tenant'].':'.$this->f['company']]]);
$connection=config('database.default');$original=config("database.connections.$connection.database");
config(["database.connections.$connection.database"=>'sgf_tempo_dev']);
app()->detectEnvironment(fn()=>'local');
try { $this->getJson('/api/session')->assertOk()->assertJsonPath('context.modules',[]); }
finally { config(["database.connections.$connection.database"=>$original]);app()->detectEnvironment(fn()=>'testing'); }
}
public function test_shared_records_require_explicit_association_and_company_scope(): void
{
$actor=$this->f['manager'];
foreach(['clients.view','clients.create','clients.edit'] as $p)IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],$p);
IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']);
$this->actingAs($actor)->withSession(['auth_version'=>$actor->auth_version]);
$token=$this->select('commercial')->assertOk()->json('context.token');
$this->withHeaders(['X-SGF-Context'=>$token,'Idempotency-Key'=>'module-client-proof-001']);
$id=$this->postJson('/api/a/clients',['name'=>'Module proof client'])->assertCreated()->json('id');
$this->getJson('/api/a/clients')->assertOk()->assertJsonPath('records.0.id',$id);
$token=$this->select('workshop')->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[]);
$this->getJson('/api/a/clients/'.$id)->assertForbidden();
$this->getJson('/api/module-records')->assertOk()->assertJsonPath('records.clients.0.id',$id);
$this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'workshop','reason'=>'Explicit synthetic association'])->assertOk();
$this->getJson('/api/a/clients/'.$id)->assertOk()->assertJsonPath('name','Module proof client');
$this->assertSame(1,DB::table('a_clients')->where('id',$id)->value('version'));
$this->assertSame(2,DB::table('identity_audit')->where('action','module.record.associated')->where('company_id',$this->f['company'])->count());
$this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'workshop','reason'=>'Correct association without rewriting original','operation'=>'remove'])->assertOk();
$this->getJson('/api/a/clients/'.$id)->assertForbidden();
$this->getJson('/api/module-records')->assertOk()->assertJsonPath('records.clients.0.modules',['commercial']);
$this->assertSame(1,DB::table('a_clients')->where('id',$id)->value('version'));
$this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'workshop','reason'=>'Re-association explicitly reviewed'])->assertOk();
$this->getJson('/api/a/clients/'.$id)->assertOk();
IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['sibling'],'clients.view');
$token=$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['sibling'],'unit_id'=>$this->f['siblingUnit'],'module_id'=>'commercial'])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients/'.$id)->assertForbidden();
$this->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[]);
}
public function test_unassociated_legacy_records_remain_in_explicit_administrative_inventory(): void
{
$id=DB::table('a_clients')->insertGetId(['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>null,'name'=>'Existing unassociated record','version'=>1,'data'=>'{}','created_at'=>now(),'updated_at'=>now()]);
$token=$this->select('commercial')->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[])->assertJsonStructure(['scope_notice']);
$this->getJson('/api/a/clients/'.$id)->assertForbidden();
$this->getJson('/api/module-records')->assertForbidden();
IdentityFixture::grant($this->f['reader']->id,$this->f['tenant'],$this->f['company'],'access.manage');
$this->getJson('/api/module-records')->assertOk()->assertJsonPath('records.clients.0.id',$id)->assertJsonPath('records.clients.0.modules',[]);
$this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'commercial','reason'=>'Cannot assign without edit permission'])->assertForbidden();
$this->assertSame('Existing unassociated record',DB::table('a_clients')->where('id',$id)->value('name'));
}
public function test_draft_creation_preserves_source_history_and_does_not_share_dependencies_implicitly(): void
{
$actor=$this->f['manager'];
foreach(['clients','services','service_drafts','billing_drafts'] as $area){
foreach(['view','create','edit'] as $action)IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],"$area.$action",str_ends_with($area,'_drafts')?$this->f['unit']:null);
}
IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']);
$this->actingAs($actor)->withSession(['auth_version'=>$actor->auth_version]);
$token=$this->select('commercial')->assertOk()->json('context.token');
$write=function($area,$body)use($token){return $this->withHeaders(['X-SGF-Context'=>$token,'Idempotency-Key'=>(string)\Illuminate\Support\Str::uuid()])->postJson('/api/a/'.$area,$body)->assertCreated()->json('id');};
$client=$write('clients',['name'=>'Synthetic modular client']);
$service=$write('services',['name'=>'Synthetic modular service']);
$draft=$write('service_drafts',['client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$service,'quantity'=>'2','unit_price'=>'75']]]);
$bill=$write('billing_drafts',['source_id'=>$draft]);
$original=$this->getJson('/api/a/billing_drafts/'.$bill)->assertOk()->json();
$this->assertSame($draft,$original['source_id']);
$token=$this->select('workshop')->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context',$token)->postJson('/api/module-records',['area'=>'billing_drafts','id'=>$bill,'version'=>99,'module_id'=>'workshop','reason'=>'Reject stale review'])->assertStatus(409);
$this->postJson('/api/module-records',['area'=>'billing_drafts','id'=>$bill,'version'=>1,'module_id'=>'workshop','reason'=>'Explicitly share this preserved draft'])->assertOk();
$record=$this->getJson('/api/a/billing_drafts/'.$bill)->assertOk()->json();
$this->assertSame($original['data'],$record['data']);
$this->assertSame($original['history'],$record['history']);
$this->assertSame($draft,$record['source_id']);
$this->getJson('/api/a/service_drafts/'.$draft)->assertForbidden();
$this->getJson('/api/a/clients/'.$client)->assertForbidden();
$this->getJson('/api/a/services/'.$service)->assertForbidden();
$this->assertSame(1,DB::table('a_billing_drafts')->where('id',$bill)->value('version'));
}
}