Files
Investidor-V2/tests/Feature/PartnershipRoomRecoveryTest.php

215 lines
14 KiB
PHP

<?php
namespace Tests\Feature;
use App\Jobs\ScanDocumentForMalware;
use App\Models\User;
use App\Services\PartnershipExchange;
use App\Services\PartnershipRoomUploads;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;
final class PartnershipRoomRecoveryTest extends TestCase
{
use RefreshDatabase;
private int $roomId;
private User $owner;
private User $candidate;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Http::fake();
Mail::fake();
Queue::fake();
Storage::fake('local');
$this->owner = User::factory()->create();
$this->candidate = User::factory()->create();
$service = app(PartnershipExchange::class);
$record = $service->save($this->owner, ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC recovery', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC offer', 'seeking' => 'SYNTHETIC cooperation', 'payload' => ['consent' => true]]);
$record = $service->transition($this->owner, $record->id, $record->version, 'submit', 'SYNTHETIC submission');
$moderator = User::factory()->create();
config(['portal.managers' => [$moderator->email]]);
$service->transition($moderator, $record->id, $record->version, 'publish', 'SYNTHETIC moderation');
$interest = $service->interest($this->candidate, $record->id, ['proposal' => 'SYNTHETIC proposal', 'payload' => ['consent' => true]]);
$interest = $service->decideInterest($this->owner, $interest->id, $interest->version, 'shortlist', 'SYNTHETIC shortlist');
$service->decideInterest($this->owner, $interest->id, $interest->version, 'connect', 'SYNTHETIC connection');
$this->roomId = (int) DB::table('partnership_rooms')->where('interest_id', $interest->id)->value('id');
$this->actingAs($this->owner)->postJson($this->url('acordo'), ['version' => 1, 'action' => 'record', 'title' => 'SYNTHETIC plan', 'terms' => 'SYNTHETIC terms', 'reason' => 'SYNTHETIC record'])->assertOk();
}
private function url(string $suffix = ''): string
{
return '/parcerias/salas/'.$this->roomId.($suffix ? '/'.$suffix : '');
}
private function milestone(): array
{
return ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC deliverable', 'responsible_id' => $this->candidate->id, 'due_date' => null, 'status' => 'pending', 'evidence' => null];
}
private function upload(string $key, bool $shared = false, string $content = 'SYNTHETIC PDF', string $name = 'synthetic-proof.pdf'): array
{
return ['client_key' => $key, 'shared' => $shared, 'file' => UploadedFile::fake()->createWithContent($name, "%PDF-1.4\n".$content."\n%%EOF")];
}
private function closeRoom(): void
{
$this->postJson($this->url('acordo'), ['version' => 2, 'action' => 'cancel', 'reason' => 'SYNTHETIC closed while response uncertain'])->assertOk();
}
public function test_creation_replay_has_one_milestone_event_audit_and_notification_set(): void
{
$input = $this->milestone();
$events = DB::table('partnership_events')->count();
$audits = DB::table('audit_events')->count();
$notifications = DB::table('partnership_notifications')->count();
$first = $this->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('replayed', false)->json('milestone_id');
$this->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('milestone_id', $first)->assertJsonPath('version', 1);
$this->postJson($this->url('marcos'), [...$input, 'client_key' => strtoupper($input['client_key'])])->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('milestone_id', $first);
$this->assertDatabaseCount('partnership_milestones', 1);
$this->assertSame($events + 1, DB::table('partnership_events')->count());
$this->assertSame($audits + 1, DB::table('audit_events')->count());
$this->assertSame($notifications + 2, DB::table('partnership_notifications')->count());
$this->getJson($this->url())->assertOk()->assertJsonPath('milestones.0.client_key', $input['client_key']);
$this->actingAs($this->candidate)->getJson($this->url())->assertOk()->assertJsonPath('milestones.0.client_key', null);
}
public function test_creation_replay_after_update_and_closure_never_rewrites_the_milestone(): void
{
$input = $this->milestone();
$id = $this->postJson($this->url('marcos'), $input)->assertOk()->json('milestone_id');
$this->putJson($this->url('marcos/'.$id), [...$input, 'title' => 'SYNTHETIC revised deliverable', 'version' => 1])->assertOk();
$this->closeRoom();
$events = DB::table('partnership_events')->count();
$this->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('milestone_id', $id)->assertJsonPath('version', 2)->assertJsonPath('replayed', true);
$this->assertDatabaseHas('partnership_milestones', ['id' => $id, 'title' => 'SYNTHETIC revised deliverable', 'version' => 2]);
$this->postJson($this->url('marcos'), [...$input, 'client_key' => (string) Str::uuid()])->assertConflict();
$this->assertSame($events, DB::table('partnership_events')->count());
}
public function test_milestone_keys_are_content_bound_scoped_per_actor_and_legacy_requests_still_work(): void
{
$input = $this->milestone();
$this->postJson($this->url('marcos'), $input)->assertOk();
$this->postJson($this->url('marcos'), [...$input, 'title' => 'SYNTHETIC different command'])->assertConflict();
$this->postJson($this->url('marcos'), [...$input, 'client_key' => 'not-a-uuid'])->assertUnprocessable();
$this->actingAs($this->candidate)->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('replayed', false);
unset($input['client_key']);
$this->postJson($this->url('marcos'), $input)->assertOk();
$this->assertDatabaseCount('partnership_milestones', 3);
}
public function test_recovery_does_not_restore_revoked_representation_or_allow_a_read_only_actor(): void
{
$input = $this->milestone();
$this->postJson($this->url('marcos'), $input)->assertOk();
$record = DB::table('partnership_rooms as r')->join('partnership_interests as i', 'i.id', '=', 'r.interest_id')->where('r.id', $this->roomId)->value('i.partnership_id');
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC new representation', 'organization_type' => 'private']);
DB::table('portal_partnerships')->where('id', $record)->update(['organization_id' => $org]);
DB::table('memberships')->insert(['organization_id' => $org, 'user_id' => $this->owner->id, 'role' => 'viewer']);
$this->getJson($this->url())->assertOk()->assertJsonPath('canWrite', false);
$this->postJson($this->url('marcos'), $input)->assertForbidden();
DB::table('memberships')->where('user_id', $this->owner->id)->update(['deleted_at' => now()]);
$this->postJson($this->url('marcos'), $input)->assertForbidden();
$this->assertDatabaseCount('partnership_milestones', 1);
}
public function test_upload_replay_preserves_original_bytes_one_link_and_one_scan_dispatch(): void
{
$key = (string) Str::uuid();
$events = DB::table('partnership_events')->count();
$id = $this->postJson($this->url('documentos'), $this->upload($key))->assertOk()->assertJsonPath('replayed', false)->json('document_id');
$this->postJson($this->url('documentos'), $this->upload($key))->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('document_id', $id);
$this->postJson($this->url('documentos'), $this->upload(strtoupper($key)))->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('document_id', $id);
$this->postJson($this->url('documentos'), $this->upload($key, true))->assertConflict();
$this->postJson($this->url('documentos'), $this->upload($key, false, 'SYNTHETIC different bytes'))->assertConflict();
$this->postJson($this->url('documentos'), $this->upload($key, false, 'SYNTHETIC PDF', 'renamed.pdf'))->assertConflict();
$this->assertDatabaseCount('documents', 1);
$this->assertDatabaseCount('partnership_documents', 1);
$this->assertCount(1, Storage::disk('local')->allFiles());
Queue::assertPushed(ScanDocumentForMalware::class, 1);
$this->assertSame($events + 1, DB::table('partnership_events')->count());
$document = DB::table('documents')->where('id', $id)->first();
$this->assertSame('quarantined', $document->status);
$this->assertSame($document->sha256, hash_file('sha256', Storage::disk('local')->path($document->storage_key)));
}
public function test_upload_recovery_after_revocation_and_closure_does_not_reshare_or_requeue(): void
{
$key = (string) Str::uuid();
$id = $this->postJson($this->url('documentos'), $this->upload($key, true))->assertOk()->json('document_id');
$link = DB::table('partnership_documents')->where('document_id', $id)->first();
$this->postJson($this->url('documentos/'.$link->id.'/partilha'), ['shared' => false])->assertOk();
$this->closeRoom();
$events = DB::table('partnership_events')->count();
$this->postJson($this->url('documentos'), $this->upload($key, true))->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('document_id', $id);
$this->assertDatabaseHas('partnership_documents', ['id' => $link->id, 'shared' => false]);
Queue::assertPushed(ScanDocumentForMalware::class, 1);
$this->assertSame($events, DB::table('partnership_events')->count());
$this->postJson($this->url('documentos'), $this->upload((string) Str::uuid()))->assertConflict();
$this->actingAs($this->candidate)->getJson($this->url())->assertOk()->assertJsonCount(0, 'documents');
$this->postJson($this->url('documentos'), $this->upload($key, true))->assertConflict();
}
public function test_message_recovery_after_closure_is_read_only_and_keys_are_not_disclosed_to_the_other_party(): void
{
$input = ['client_key' => (string) Str::uuid(), 'body' => 'SYNTHETIC uncertain message'];
$this->postJson($this->url('mensagens'), $input)->assertOk();
$this->getJson($this->url())->assertOk()->assertJsonPath('messages.0.client_key', $input['client_key']);
$this->actingAs($this->candidate)->getJson($this->url())->assertOk()->assertJsonPath('messages.0.client_key', null);
$this->actingAs($this->owner);
$this->closeRoom();
$events = DB::table('partnership_events')->count();
$this->postJson($this->url('mensagens'), $input)->assertOk();
$this->postJson($this->url('mensagens'), [...$input, 'body' => 'SYNTHETIC different content'])->assertConflict();
$this->postJson($this->url('mensagens'), [...$input, 'client_key' => (string) Str::uuid()])->assertConflict();
$this->assertDatabaseCount('partnership_messages', 1);
$this->assertSame($events, DB::table('partnership_events')->count());
}
public function test_stale_agreement_and_milestone_updates_preserve_the_current_state_and_evidence(): void
{
$input = $this->milestone();
$id = $this->postJson($this->url('marcos'), $input)->assertOk()->json('milestone_id');
$this->putJson($this->url('marcos/'.$id), [...$input, 'version' => 1, 'status' => 'completed', 'evidence' => 'SYNTHETIC frozen result'])->assertOk();
$this->putJson($this->url('marcos/'.$id), [...$input, 'version' => 1])->assertConflict();
$this->postJson($this->url('acordo'), ['version' => 1, 'action' => 'record', 'title' => 'SYNTHETIC stale', 'terms' => 'SYNTHETIC stale terms', 'reason' => 'SYNTHETIC stale attempt'])->assertConflict();
$this->assertDatabaseHas('partnership_milestones', ['id' => $id, 'version' => 2, 'status' => 'completed', 'evidence' => 'SYNTHETIC frozen result']);
$this->assertDatabaseHas('partnership_rooms', ['id' => $this->roomId, 'version' => 2, 'status' => 'agreement_recorded']);
}
public function test_failed_document_event_rolls_back_the_new_file_without_removing_an_existing_original(): void
{
$id = $this->postJson($this->url('documentos'), $this->upload((string) Str::uuid()))->assertOk()->json('document_id');
$original = DB::table('documents')->where('id', $id)->first();
$exchange = \Mockery::mock(PartnershipExchange::class)->makePartial();
$exchange->shouldReceive('event')->once()->andThrow(new \RuntimeException('SYNTHETIC event failure'));
$this->app->instance(PartnershipExchange::class, $exchange);
$input = $this->upload((string) Str::uuid(), false, 'SYNTHETIC failed second upload');
try {
app(PartnershipRoomUploads::class)->save($this->owner, $this->roomId, $input['file'], false, $input['client_key']);
$this->fail('The synthetic event failure should abort the new upload.');
} catch (\RuntimeException $error) {
$this->assertSame('SYNTHETIC event failure', $error->getMessage());
}
$this->assertDatabaseCount('documents', 1);
$this->assertDatabaseCount('partnership_documents', 1);
$this->assertCount(1, Storage::disk('local')->allFiles());
$this->assertSame($original->sha256, hash_file('sha256', Storage::disk('local')->path($original->storage_key)));
Queue::assertPushed(ScanDocumentForMalware::class, 1);
}
}