98 lines
8.0 KiB
PHP
98 lines
8.0 KiB
PHP
<?php
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use App\Services\OpportunityManagement;
|
|
use App\Services\ProactiveSupply;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Support\ProactiveInvestmentFixture as F;
|
|
use Tests\TestCase;
|
|
|
|
final class ProactiveSupplyTest extends TestCase
|
|
{
|
|
use DatabaseTransactions;
|
|
private User $actor;
|
|
private string $territory;
|
|
private string $observation;
|
|
protected function setUp():void
|
|
{
|
|
parent::setUp();F::guard();$this->withoutVite();
|
|
config(['portal.profile_access_enabled'=>true,'portal.opportunity_specialized_drafts_enabled'=>true,'portal.managers'=>[],'investor-path.enabled'=>true,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false]);
|
|
$this->actor=F::operator();$this->login($this->actor);
|
|
$input=F::input('Gurué');$this->territory=$input['district_id'];
|
|
$hash=hash('sha256','SYNTHETIC-QA-SUPPLY-SOURCE');
|
|
DB::table('proactive_supply_sources')->insert(['sha256'=>$hash,'name'=>'SYNTHETIC-QA-SUPPLY.xlsx','period'=>'2020','limitations'=>'Synthetic, not a real market source','receipt'=>'{}','imported_at'=>now()]);
|
|
$this->observation=hash('sha256','SYNTHETIC-QA-OBSERVATION');
|
|
$row=['id'=>$this->observation,'source_hash'=>$hash,'sheet'=>'QA','row_number'=>2,'entity_key'=>hash('sha256','QA-ENTITY'),'name'=>'PRIVATE-UNIQUE-QA-SUPPLIER','province'=>'Zambézia','district'=>'Gurué','cae'=>'33120','activity'=>'SYNTHETIC QA repair','sector'=>'SYNTHETIC QA manufacturing','source_date'=>'2020','fingerprint'=>hash('sha256','QA-FINGERPRINT'),'duplicate_of'=>null,'identity_conflict'=>false,'territory_id'=>$this->territory,'territory_resolution'=>'exact_normalized_province_district'];
|
|
DB::table('proactive_supply_observations')->insert($row);
|
|
DB::table('proactive_supply_observations')->insert([...$row,'id'=>hash('sha256','QA-REPEATED-OBSERVATION'),'row_number'=>3,'duplicate_of'=>$this->observation]);
|
|
}
|
|
private function login(User $u):void{$this->actingAs($u)->withSession(['portal.mfa_actor'=>$u->id,'portal.mfa_at'=>time(),'portal.access_revision'=>(int)$u->portal_access_revision]);}
|
|
private function url(string $extra=''):string{return '/minhas-oportunidades/oferta-interna?territory_id='.$this->territory.$extra;}
|
|
public function test_private_query_filters_supply_and_preserves_source_and_duplicate_count():void
|
|
{
|
|
$r=$this->getJson($this->url('&activity=SYNTHETIC%20QA%20repair'))->assertOk()->assertJsonPath('total',1)->assertJsonPath('duplicate_sightings',1)->assertJsonPath('rows.0.source_name','SYNTHETIC-QA-SUPPLY.xlsx')->assertJsonPath('rows.0.row_number',2)->assertJsonPath('rows.0.source_date','2020');
|
|
$this->assertStringContainsString('no-store',$r->headers->get('Cache-Control'));
|
|
$this->assertArrayNotHasKey('contact',$r->json('rows.0'));
|
|
$this->assertArrayNotHasKey('nuit',$r->json('rows.0'));
|
|
}
|
|
public function test_no_match_means_insufficient_information_not_absent_market():void
|
|
{
|
|
$this->getJson($this->url('&activity=NO-SUCH-SYNTHETIC-ACTIVITY'))->assertOk()->assertJsonPath('total',0)->assertJsonPath('status','Informação insuficiente nesta base e nestes filtros.');
|
|
}
|
|
public function test_unassigned_user_cannot_read_internal_register():void
|
|
{
|
|
$this->login(User::factory()->create());$this->getJson($this->url())->assertForbidden();
|
|
$this->getJson('/minhas-oportunidades/oferta-interna?territory_id=not-a-uuid')->assertForbidden();
|
|
}
|
|
public function test_comparison_persists_source_lineage_and_rejects_forgery_and_public_reclassification():void
|
|
{
|
|
$input=F::input('Gurué');$a=$input['proactive'];$id=(string)\Illuminate\Support\Str::uuid();
|
|
$a['evidence'][]=['id'=>$id,'interpretation'=>'SYNTHETIC QA possible complement, not a contract','verified_on'=>'2026-10-28',...app(ProactiveSupply::class)->evidence($this->actor,$this->observation,null)];
|
|
$a['supply_comparisons']=[['observation_id'=>$this->observation,'relationship'=>'complementarity','rationale'=>'SYNTHETIC QA compare servicing with tea-chain idea','limitations'=>'No current capacity or demand proved']];
|
|
$input['proactive']=$a;
|
|
$oid=$this->postJson('/minhas-oportunidades/drafts',$input)->assertCreated()->json('id');$o=Opportunity::findOrFail($oid);
|
|
$saved=app(OpportunityManagement::class)->values($o)['proactive'];$this->assertSame($this->observation,$saved['supply_comparisons'][0]['observation_id']);
|
|
$this->assertSame('internal',$saved['evidence'][2]['access']);$this->assertStringContainsString('SYNTHETIC-QA-SUPPLY.xlsx',$saved['evidence'][2]['source']);
|
|
$a['evidence'][2]['source']='Forged source';$a['evidence'][2]['statement']='Forged supply fact';
|
|
$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA canonical source lineage is server-resolved'])->assertOk();
|
|
$saved=app(OpportunityManagement::class)->values($o->fresh())['proactive'];$this->assertStringContainsString('PRIVATE-UNIQUE-QA-SUPPLIER',$saved['evidence'][2]['statement']);
|
|
$a['evidence'][2]['access']='public';$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA internal list cannot become public'])->assertUnprocessable();
|
|
$a['evidence'][2]['access']='internal';$a['supply_comparisons'][0]['observation_id']=str_repeat('f',64);
|
|
$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA unknown supply observation rejected'])->assertUnprocessable();
|
|
}
|
|
public function test_sector_filter_does_not_infer_absence_and_comparison_requires_its_evidence():void
|
|
{
|
|
$this->getJson($this->url('§or=NO-SUCH-SECTOR'))->assertOk()->assertJsonPath('total',0);
|
|
$input=F::input('Gurué');$input['proactive']['supply_comparisons']=[['observation_id'=>$this->observation,'relationship'=>'partner','rationale'=>'SYNTHETIC QA prospective partner','limitations'=>'Not contacted, no agreement']];
|
|
$this->postJson('/minhas-oportunidades/drafts',$input)->assertUnprocessable();
|
|
}
|
|
public function test_database_policy_hides_internal_rows_without_actor_context():void
|
|
{
|
|
$role='supply_qa_'.bin2hex(random_bytes(6));
|
|
DB::statement('CREATE ROLE '.$role.' NOLOGIN');
|
|
DB::statement('GRANT USAGE ON SCHEMA public TO '.$role);
|
|
DB::statement('GRANT SELECT ON proactive_supply_sources,proactive_supply_observations TO '.$role);
|
|
DB::select("select set_config('app.actor_user_id','',true),set_config('app.proactive_supply_read','',true)");
|
|
try{
|
|
DB::statement('SET LOCAL ROLE '.$role);
|
|
$this->assertSame(0,DB::table('proactive_supply_observations')->count());
|
|
$this->assertSame(0,DB::table('proactive_supply_sources')->count());
|
|
DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)",[(string)$this->actor->id]);
|
|
$this->assertNotNull(DB::table('proactive_supply_observations')->where('id',$this->observation)->first());
|
|
}finally{DB::statement('RESET ROLE');}
|
|
}
|
|
public function test_preserved_repetitions_cannot_inflate_saved_comparisons():void
|
|
{
|
|
$this->expectException(\Illuminate\Validation\ValidationException::class);
|
|
$this->expectExceptionMessage('Esta observação administrativa é repetida.');
|
|
$comparison=['relationship'=>'competition','rationale'=>'SYNTHETIC QA duplicate must not count as another competitor','limitations'=>'Historical observation,not a unique company'];
|
|
app(ProactiveSupply::class)->comparisons($this->actor,[
|
|
[...$comparison,'observation_id'=>$this->observation],
|
|
[...$comparison,'observation_id'=>hash('sha256','QA-REPEATED-OBSERVATION')],
|
|
],null);
|
|
}
|
|
}
|