Files
Investidor-V2/tests/Feature/OpportunityOriginReviewTest.php
T

96 lines
6.1 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\Project;
use App\Models\Sector;
use App\Models\User;
use App\Services\InvestmentCatalogue;
use App\Services\OpportunityOriginReview;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class OpportunityOriginReviewTest extends TestCase
{
use RefreshDatabase;
private string $directory;
protected function setUp(): void
{
parent::setUp();
$this->directory = storage_path('framework/testing/origin-'.Str::uuid());
mkdir($this->directory, 0700, true);
file_put_contents($this->directory.'/dump', 'QA-only backup fixture');
config(['opportunity_origin.proof_path' => $this->directory.'/proof.json']);
file_put_contents($this->directory.'/proof.json', json_encode(['restore_verified' => true, 'reversal_verified' => true, 'code_hash' => app(OpportunityOriginReview::class)->fingerprint(), 'verified_at' => gmdate('c'), 'target' => ['database' => config('database.connections.pgsql.database'), 'port' => 55441], 'dump_path' => $this->directory.'/dump', 'sha256' => hash_file('sha256', $this->directory.'/dump')]));
}
protected function tearDown(): void
{
foreach (glob($this->directory.'/*') ?: [] as $file) {
unlink($file);
}
rmdir($this->directory);
parent::tearDown();
}
private function fixture(): array
{
$sector = Sector::create(['name' => 'QA sector', 'slug' => 'qa-origin', 'is_active' => true]);
$opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'INV-MZ-AGR-001', 'title' => 'Synthetic QA opportunity', 'description' => 'Synthetic QA fixture only', 'slug' => 'qa-origin', 'status' => 'published', 'published_at' => now()->subMinute()]);
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$project = Project::factory()->create();
DB::table('opportunity_projects')->insert(['opportunity_id' => $opportunity->id, 'project_id' => $project->id]);
DB::table('opportunity_favourites')->insert(['user_id' => $manager->id, 'opportunity_id' => $opportunity->id]);
return compact('opportunity', 'manager', 'project');
}
public function test_preview_has_no_decision_and_example_separation_preserves_records_and_interactions(): void
{
$f = $this->fixture();
$before = $f['opportunity']->fresh()->getRawOriginal();
$payload = ['id' => $f['opportunity']->id, 'classification' => 'example', 'reason' => 'Responsible QA declaration: synthetic fixture'];
$preview = $this->actingAs($f['manager'])->postJson('/management/opportunity-origin/preview', $payload)->assertOk()->assertJsonPath('relationships_changed', false)->json();
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->count());
$this->assertSame('undetermined', app(OpportunityOriginReview::class)->state($f['opportunity']->id)['classification']);
$apply = [...$payload, 'snapshot_hash' => $preview['current']['snapshot_hash'], 'confirm' => true];
$this->postJson('/management/opportunity-origin/apply', $apply)->assertOk()->assertJsonPath('changed', 1);
$this->postJson('/management/opportunity-origin/apply', $apply)->assertOk()->assertJsonPath('replayed', true);
$this->assertSame([], app(InvestmentCatalogue::class)->query([])->pluck('id')->all());
$this->get('/oportunidades/qa-origin')->assertNotFound();
$this->getJson('/api/v1/opportunities/qa-origin')->assertNotFound();
$this->assertSame(0, app(InvestmentCatalogue::class)->map([])['matched_count']);
$this->get('/management/opportunity-origin')->assertOk();
$this->assertSame($before, $f['opportunity']->fresh()->getRawOriginal());
$this->assertDatabaseHas('opportunity_projects', ['opportunity_id' => $f['opportunity']->id, 'project_id' => $f['project']->id]);
$this->assertDatabaseHas('opportunity_favourites', ['opportunity_id' => $f['opportunity']->id, 'user_id' => $f['manager']->id]);
$state = app(OpportunityOriginReview::class)->state($f['opportunity']->id);
$this->assertSame($f['manager']->id, $state['history'][0]['author']);
$this->assertSame($payload['reason'], $state['history'][0]['reason']);
$this->postJson('/management/opportunity-origin/apply', ['id' => $f['opportunity']->id, 'classification' => 'undetermined', 'reason' => 'QA restore previous classification', 'snapshot_hash' => $state['snapshot_hash'], 'confirm' => true])->assertOk();
$this->assertTrue(Opportunity::published()->whereKey($f['opportunity']->id)->exists());
$this->assertCount(2, app(OpportunityOriginReview::class)->state($f['opportunity']->id)['history']);
}
public function test_permissions_foundation_confirmation_and_conflicts_are_required(): void
{
$f = $this->fixture();
$payload = ['id' => $f['opportunity']->id, 'classification' => 'real', 'reason' => 'QA declaration'];
$this->actingAs(User::factory()->create())->postJson('/management/opportunity-origin/preview', $payload)->assertForbidden();
$this->actingAs($f['manager'])->postJson('/management/opportunity-origin/preview', [...$payload, 'reason' => ''])->assertUnprocessable();
$state = app(OpportunityOriginReview::class)->state($f['opportunity']->id);
$this->postJson('/management/opportunity-origin/apply', [...$payload, 'snapshot_hash' => $state['snapshot_hash']])->assertUnprocessable();
$f['opportunity']->update(['description' => 'QA correction']);
$this->postJson('/management/opportunity-origin/apply', [...$payload, 'snapshot_hash' => $state['snapshot_hash'], 'confirm' => true])->assertConflict();
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->count());
unlink($this->directory.'/proof.json');
$this->postJson('/management/opportunity-origin/preview', $payload)->assertOk()->assertJsonPath('ready', false);
}
}