349 lines
24 KiB
PHP
349 lines
24 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Jobs\ScanDocumentForMalware;
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use App\Services\OpportunityManagement;
|
|
use App\Services\PortalProfileAdministration;
|
|
use App\Services\PrivateOfferConfidentiality;
|
|
use App\Services\PrivateOfferPreparation;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Foundation\Vite;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Testing\TestResponse;
|
|
use Tests\Support\CleanScannerFixture;
|
|
use Tests\TestCase;
|
|
|
|
final class PrivateOfferConfidentialityTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const BASE = '/minhas-oportunidades/ofertas';
|
|
|
|
private const REASON = 'Operação exclusivamente sintética na base QA isolada.';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
Storage::fake('local');
|
|
Queue::fake();
|
|
Mail::fake();
|
|
config(['portal.profile_access_enabled' => true, 'editorial_workflow.enabled' => true, 'private_offers.nda' => null]);
|
|
}
|
|
|
|
private function actor(?string $role = null, string $category = 'business_sale'): User
|
|
{
|
|
$actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('SYNTHETIC-QA'), 'two_factor_confirmed_at' => now()]);
|
|
if ($role) {
|
|
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), [
|
|
'user_id' => $actor->id, 'role' => $role, 'organization_id' => null, 'responsibilities' => [], 'opportunity_types' => [$category], 'source' => 'synthetic-test-fixture']);
|
|
}
|
|
|
|
return $actor;
|
|
}
|
|
|
|
private function login(User $actor): static
|
|
{
|
|
$actor = $actor->fresh();
|
|
|
|
return $this->actingAs($actor)->withSession(['login.id' => null, 'portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->fresh()->portal_access_revision]);
|
|
}
|
|
|
|
private function draft(User $owner, string $category = 'business_sale', array $details = []): Opportunity
|
|
{
|
|
$contact = (string) Str::uuid();
|
|
DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'PRIVATE-CONTACT-CANARY', 'source_system' => 'portal-user-contact', 'external_id' => 'user:'.$owner->id]);
|
|
$result = $this->login($owner)->postJson(self::BASE.'/rascunhos', ['creation_key' => (string) Str::uuid(), 'title' => 'Synthetic QA private offer',
|
|
'description' => 'PRIVATE-DESCRIPTION-CANARY', 'opportunity_category' => $category,
|
|
'private_offer' => ['price_mode' => 'amount', 'amount' => '100.01', 'currency' => 'MZN', 'contact_id' => $contact, ...$details], 'reason' => self::REASON])->assertCreated();
|
|
|
|
return Opportunity::findOrFail($result->json('record.id'));
|
|
}
|
|
|
|
private function etag(Opportunity $record): string
|
|
{
|
|
return app(OpportunityManagement::class)->etag($record->fresh());
|
|
}
|
|
|
|
private function approve(User $owner, Opportunity $record): void
|
|
{
|
|
$this->login($owner)->postJson(self::BASE.'/'.$record->id.'/submeter', ['etag' => $this->etag($record), 'confirm' => true])->assertOk();
|
|
$reviewer = $this->actor('validator', $record->opportunity_category);
|
|
foreach (['start_review', 'approve'] as $action) {
|
|
$this->login($reviewer)->postJson(self::BASE.'/'.$record->id.'/revisao', ['etag' => $this->etag($record), 'action' => $action, 'reason' => self::REASON])->assertOk();
|
|
}
|
|
}
|
|
|
|
private function nda(): array
|
|
{
|
|
// Deliberately non-legal bytes, never registered as an operational NDA.
|
|
$bytes = "%PDF-1.7\nSYNTHETIC QA - NOT A LEGAL AGREEMENT\n%%EOF";
|
|
$id = (string) Str::uuid();
|
|
$sha = hash('sha256', $bytes);
|
|
Storage::disk('local')->put('qa-nda/'.$id, Crypt::encryptString($bytes));
|
|
DB::table('documents')->insert(['id' => $id, 'file_name' => 'synthetic-qa.pdf', 'media_type' => 'application/pdf', 'storage_key' => 'qa-nda/'.$id,
|
|
'storage_encryption' => 'laravel', 'sha256' => $sha, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $sha,
|
|
'malware_engine' => 'QA-Test-Double', 'version_no' => 1, 'data_classification' => 'restricted']);
|
|
config(['private_offers.nda' => ['document_id' => $id, 'version' => 1, 'sha256' => $sha, 'approval_reference' => 'SYNTHETIC-QA-NOT-LEGAL']]);
|
|
|
|
return app(PrivateOfferConfidentiality::class)->nda();
|
|
}
|
|
|
|
private function command(Opportunity $offer, array $nda, string $action, array $extra = []): array
|
|
{
|
|
return ['action' => $action, 'version_hash' => $offer->fresh()->workflow_context['approval']['hash'], 'nda_sha256' => $nda['sha256'],
|
|
'nda_fingerprint' => $nda['fingerprint'], 'confirm' => true, 'reason' => self::REASON, ...$extra];
|
|
}
|
|
|
|
public function test_confirmed_specific_fields_save_recover_and_bind_the_editorial_version(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner, details: ['business' => ['revenue' => '10000000000000.01', 'cash_flow' => '-123.45', 'currency' => 'USD', 'reason' => 'PRIVATE-FINANCIAL-CANARY']]);
|
|
$this->login($owner)->getJson(self::BASE.'/'.$record->id)->assertOk()->assertJsonPath('record.values.private_offer.business.cash_flow', '-123.45');
|
|
$this->approve($owner, $record);
|
|
$this->assertSame('private-offer-editorial-v3', $record->fresh()->workflow_context['submission']['contract_version']);
|
|
$this->assertSame('-123.45', $record->fresh()->workflow_context['submission']['payload']['private_offer']['business']['cash_flow']);
|
|
$this->login($owner)->postJson(self::BASE.'/'.$record->id.'/guardar', ['etag' => $this->etag($record), 'private_offer' => ['business' => ['reason' => 'Changed']], 'reason' => self::REASON])->assertForbidden();
|
|
}
|
|
|
|
public function test_property_fields_are_optional_and_categories_and_unknown_fields_are_enforced(): void
|
|
{
|
|
$owner = $this->actor('promoter', 'real_estate');
|
|
$record = $this->draft($owner, 'real_estate', ['property' => ['transaction_type' => 'lease', 'property_type' => 'house', 'built_area' => '120.50', 'area_unit' => 'sqm', 'bedrooms' => 3, 'bathrooms' => 2, 'amenities' => ['Água', 'Acesso']]]);
|
|
$this->login($owner)->getJson(self::BASE.'/'.$record->id)->assertOk()->assertJsonPath('record.values.private_offer.property.bedrooms', 3);
|
|
foreach ([['business' => ['revenue' => '5', 'currency' => 'USD']], ['property' => ['title_certified' => true]], ['property' => ['property_type' => 'invented']]] as $invalid) {
|
|
$this->postJson(self::BASE.'/'.$record->id.'/guardar', ['etag' => $this->etag($record), 'private_offer' => $invalid, 'reason' => self::REASON])->assertUnprocessable();
|
|
}
|
|
$this->assertSame(3, app(OpportunityManagement::class)->values($record->fresh())['private_offer']['property']['bedrooms']);
|
|
}
|
|
|
|
public function test_six_photos_limit_includes_restoration_and_alternate_routes_and_cleans_failed_uploads(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$ids = [];
|
|
for ($index = 0; $index < 6; $index++) {
|
|
$response = $this->upload($owner, $record, 'images', $this->png('qa-'.$index.'.png', 400 + $index, 400));
|
|
$ids[] = $response->assertCreated()->json('id');
|
|
}
|
|
$this->upload($owner, $record, 'images', $this->png('seventh.png', 420, 420))->assertUnprocessable();
|
|
$this->assertDatabaseCount('opportunity_images', 6);
|
|
$this->assertCount(6, Storage::disk('local')->allFiles('opportunity-resources'));
|
|
$life = ['etag' => $this->etag($record), 'version' => 1, 'confirm' => true, 'action' => 'delete', 'reason' => self::REASON];
|
|
$this->postJson(self::BASE.'/'.$record->id.'/recursos/images/'.$ids[0], $life)->assertOk();
|
|
$this->upload($owner, $record, 'images', $this->png('new.png', 430, 430))->assertCreated();
|
|
$restore = [...$life, 'etag' => $this->etag($record), 'version' => 2, 'action' => 'restore'];
|
|
$this->postJson(self::BASE.'/'.$record->id.'/recursos/images/'.$ids[0], $restore)->assertUnprocessable();
|
|
// The accepted F1 guard closes the native promoter route for private offers.
|
|
$this->postJson('/minhas-oportunidades/'.$record->id.'/resources/images/'.$ids[0].'/lifecycle', $restore)->assertForbidden();
|
|
$this->assertSame(6, DB::table('opportunity_images')->whereNull('deleted_at')->count());
|
|
}
|
|
|
|
private function upload(User $owner, Opportunity $record, string $kind, UploadedFile $file, array $override = []): TestResponse
|
|
{
|
|
$payload = $kind === 'images' ? ['visibility' => 'internal', 'caption' => 'Synthetic QA', 'digitally_edited' => false] : ['visibility' => 'internal', 'classification' => 'restricted', 'category' => 'real_estate_proof'];
|
|
|
|
return $this->login($owner)->post(self::BASE.'/'.$record->id.'/recursos/'.$kind, ['etag' => $this->etag($record), 'creation_key' => (string) Str::uuid(), 'reason' => self::REASON,
|
|
'payload' => json_encode([...$payload, ...$override]), 'file' => $file], ['Accept' => 'application/json']);
|
|
}
|
|
|
|
/** Valid synthetic PNG bytes, independent of the optional GD extension. */
|
|
private function png(string $name, int $width, int $height): UploadedFile
|
|
{
|
|
$chunk = static fn (string $type, string $bytes): string => pack('N', strlen($bytes)).$type.$bytes.pack('N', crc32($type.$bytes));
|
|
$pixels = gzcompress(str_repeat("\0".str_repeat("\0", $width * 3), $height));
|
|
if ($pixels === false) {
|
|
throw new \RuntimeException('Synthetic PNG compression failed.');
|
|
}
|
|
|
|
return UploadedFile::fake()->createWithContent($name, "\x89PNG\r\n\x1a\n".$chunk('IHDR', pack('NNCCCCC', $width, $height, 8, 2, 0, 0, 0)).$chunk('IDAT', $pixels).$chunk('IEND', ''));
|
|
}
|
|
|
|
public function test_upload_validation_and_quarantine_do_not_grant_publication_or_read_access(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->upload($owner, $record, 'images', UploadedFile::fake()->createWithContent('fake.jpg', '<?php echo 1;'))->assertUnprocessable();
|
|
$this->upload($owner, $record, 'images', $this->png('small.png', 20, 20))->assertUnprocessable();
|
|
$this->upload($owner, $record, 'images', $this->png('edited.png', 400, 400), ['digitally_edited' => true])->assertUnprocessable();
|
|
$this->upload($owner, $record, 'images', $this->png('public.png', 400, 400), ['visibility' => 'public'])->assertUnprocessable();
|
|
$id = $this->upload($owner, $record, 'images', $this->png('good.png', 400, 400))->assertCreated()->json('id');
|
|
$this->getJson(self::BASE.'/'.$record->id.'/recursos/images/'.$id)->assertNotFound();
|
|
$this->postJson(self::BASE.'/'.$record->id.'/submeter', ['etag' => $this->etag($record), 'confirm' => true])->assertUnprocessable();
|
|
$this->assertSame('draft', $record->fresh()->status);
|
|
}
|
|
|
|
public function test_proof_bytes_are_encrypted_and_only_a_scoped_internal_validator_can_read_them(): void
|
|
{
|
|
$owner = $this->actor('promoter', 'real_estate');
|
|
$record = $this->draft($owner, 'real_estate');
|
|
$bytes = "%PDF-1.7\nSYNTHETIC QA PROPERTY PROOF - NOT A TITLE\n%%EOF";
|
|
$resourceId = $this->upload($owner, $record, 'documents', UploadedFile::fake()->createWithContent('qa.pdf', $bytes))->assertCreated()->json('id');
|
|
$link = DB::table('opportunity_documents')->where('id', $resourceId)->first();
|
|
$doc = DB::table('documents')->where('id', $link->document_id)->first();
|
|
$this->assertNotSame($bytes, Storage::disk('local')->get($doc->storage_key));
|
|
(new ScanDocumentForMalware($doc->id))->handle(new CleanScannerFixture);
|
|
$url = self::BASE.'/'.$record->id.'/recursos/documents/'.$resourceId;
|
|
$this->login($owner)->get($url)->assertForbidden();
|
|
$this->get('/minhas-oportunidades/'.$record->id.'/resources/documents/'.$resourceId.'/download')->assertForbidden();
|
|
$reviewer = $this->actor('validator', 'real_estate');
|
|
$this->login($reviewer)->get($url)->assertOk()->assertContent($bytes)->assertHeader('Cache-Control', 'no-store, private');
|
|
$this->login($this->actor('validator', 'business_sale'))->get($url)->assertForbidden();
|
|
$this->login($this->actor('auditor', 'real_estate'))->get($url)->assertForbidden();
|
|
$this->login($reviewer)->withSession(['portal.mfa_at' => time() - 1000])->get($url)->assertForbidden();
|
|
$this->assertGreaterThan(0, DB::table('audit_events')->where('action', 'offer_proof_access_authorized')->count());
|
|
$this->assertGreaterThan(0, DB::table('audit_events')->where('action', 'offer_proof_access_denied')->count());
|
|
}
|
|
|
|
public function test_nda_absence_keeps_access_closed_and_acceptance_alone_never_grants_access(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$interested = $this->actor();
|
|
$this->login($interested)->getJson(self::BASE.'/'.$record->id.'/confidencialidade')->assertOk()->assertJsonPath('nda_pending', true)->assertJsonPath('requests', []);
|
|
$this->getJson(self::BASE.'/'.$record->id.'/confidencial')->assertForbidden();
|
|
$this->get(self::BASE.'/'.$record->id.'/nda')->assertConflict();
|
|
$this->postJson(self::BASE.'/'.$record->id.'/confidencialidade', $this->command($record, ['sha256' => str_repeat('0', 64), 'fingerprint' => str_repeat('0', 64)], 'accept'))->assertConflict();
|
|
$nda = $this->nda();
|
|
$command = $this->command($record, $nda, 'accept');
|
|
unset($command['reason']); // Accepting an identified NDA does not require invented justification.
|
|
$this->postJson(self::BASE.'/'.$record->id.'/confidencialidade', $command)->assertOk();
|
|
$this->getJson(self::BASE.'/'.$record->id.'/confidencial')->assertForbidden();
|
|
$this->login($owner)->postJson(self::BASE.'/'.$record->id.'/confidencialidade', $this->command($record, $nda, 'authorize', ['interested_user_id' => $interested->id, 'acceptance_id' => (string) Str::uuid()]))->assertConflict();
|
|
}
|
|
|
|
public function test_both_conditions_exact_versions_revocation_and_audit_are_required_on_every_read(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner, details: ['business' => ['revenue' => '789.01', 'currency' => 'MZN', 'reason' => 'PRIVATE-FINANCIAL-CANARY']]);
|
|
$this->approve($owner, $record);
|
|
$nda = $this->nda();
|
|
$interested = $this->actor();
|
|
$url = self::BASE.'/'.$record->id;
|
|
$this->login($interested)->postJson($url.'/confidencialidade', $this->command($record, $nda, 'accept'))->assertOk();
|
|
$accept = $this->getJson($url.'/confidencialidade')->json('acceptance');
|
|
$grant = $this->command($record, $nda, 'authorize', ['interested_user_id' => $interested->id, 'acceptance_id' => $accept['id']]);
|
|
$this->login($this->actor('validator'))->postJson($url.'/confidencialidade', $grant)->assertForbidden();
|
|
$this->login($owner)->postJson($url.'/confidencialidade', $grant)->assertOk();
|
|
$data = $this->login($interested)->getJson($url.'/confidencial')->assertOk()->assertJsonPath('business.revenue', '789.01')->getContent();
|
|
$this->assertStringNotContainsString('PRIVATE-CONTACT-CANARY', $data);
|
|
$this->assertStringNotContainsString('PRIVATE-DESCRIPTION-CANARY', $data);
|
|
$this->login($this->actor())->getJson($url.'/confidencial')->assertForbidden();
|
|
$this->login($owner)->postJson($url.'/confidencialidade', $this->command($record, $nda, 'revoke', ['interested_user_id' => $interested->id]))->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertForbidden();
|
|
$this->assertSame(1, DB::table('audit_events')->where('action', 'offer_confidential_access')->count());
|
|
$this->assertSame(1, DB::table('audit_events')->where('action', 'offer_confidentiality_revoke')->count());
|
|
$this->assertNull($record->fresh()->published_at);
|
|
}
|
|
|
|
public function test_nda_version_changes_withdrawal_session_revocation_and_private_revision_close_access(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner, details: ['business' => ['revenue' => '900.00', 'currency' => 'USD', 'reason' => 'PRIVATE-VERSION-CANARY']]);
|
|
$this->approve($owner, $record);
|
|
$nda = $this->nda();
|
|
$interested = $this->actor();
|
|
$url = self::BASE.'/'.$record->id;
|
|
$authorize = function (array $currentNda) use ($owner, $record, $interested, $url) {
|
|
$this->login($interested)->postJson($url.'/confidencialidade', $this->command($record, $currentNda, 'accept'))->assertOk();
|
|
$acceptance = $this->getJson($url.'/confidencialidade')->json('acceptance');
|
|
$this->login($owner)->postJson($url.'/confidencialidade', $this->command($record, $currentNda, 'authorize', ['interested_user_id' => $interested->id, 'acceptance_id' => $acceptance['id']]))->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertOk();
|
|
};
|
|
$authorize($nda);
|
|
// Identical document bytes in another approved version do not reuse the acceptance.
|
|
DB::table('documents')->where('id', $nda['document_id'])->update(['version_no' => 2]);
|
|
config(['private_offers.nda.version' => 2]);
|
|
$this->getJson($url.'/confidencial')->assertForbidden();
|
|
$this->postJson($url.'/confidencialidade', $this->command($record, $nda, 'accept'))->assertConflict();
|
|
$nda = app(PrivateOfferConfidentiality::class)->nda();
|
|
$authorize($nda);
|
|
$this->postJson($url.'/confidencialidade', $this->command($record, $nda, 'withdraw'))->assertOk();
|
|
$this->getJson($url.'/confidencial')->assertForbidden();
|
|
$authorize($nda);
|
|
DB::table('users')->where('id', $interested->id)->increment('portal_access_revision');
|
|
$this->login($interested->fresh())->getJson($url.'/confidencial')->assertForbidden();
|
|
$authorize($nda);
|
|
$withdraw = $this->command($record, $nda, 'withdraw');
|
|
$this->login($owner)->postJson($url.'/revisao', ['etag' => $this->etag($record), 'action' => 'request_revision', 'reason' => self::REASON])->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertConflict();
|
|
$this->getJson($url.'/confidencialidade')->assertOk()->assertJsonPath('can_accept', false)->assertJsonPath('etag', null);
|
|
$this->postJson($url.'/confidencialidade', $withdraw)->assertOk();
|
|
$this->getJson('/api/v1/offers')->assertOk()->assertJsonPath('results.total', 0);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
|
|
$this->assertStringNotContainsString('PRIVATE-VERSION-CANARY', $this->get('/api/v1/offers/export')->assertOk()->streamedContent());
|
|
$this->getJson('/api/v1/offers/map')->assertOk()->assertJsonPath('features', []);
|
|
$this->assertSame(2, DB::table('audit_events')->where('action', 'offer_confidentiality_withdraw')->count());
|
|
}
|
|
|
|
public function test_nda_integrity_and_authority_loss_are_rechecked_without_exposing_the_company(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$interested = $this->actor();
|
|
$nda = $this->nda();
|
|
$url = self::BASE.'/'.$record->id;
|
|
$this->login($interested)->get($url.'/nda')->assertOk()->assertSee('SYNTHETIC QA - NOT A LEGAL AGREEMENT');
|
|
$this->postJson($url.'/confidencialidade', $this->command($record, $nda, 'accept'))->assertOk();
|
|
$accept = $this->getJson($url.'/confidencialidade')->json('acceptance');
|
|
$this->login($owner)->postJson($url.'/confidencialidade', $this->command($record, $nda, 'authorize', ['interested_user_id' => $interested->id, 'acceptance_id' => $accept['id']]))->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertOk();
|
|
DB::table('portal_role_assignments')->where('user_id', $owner->id)->where('role', 'promoter')->update(['revoked_at' => now()]);
|
|
$this->getJson($url.'/confidencial')->assertForbidden();
|
|
// Restore this synthetic fixture only, to isolate the byte-integrity check below.
|
|
DB::table('portal_role_assignments')->where('user_id', $owner->id)->where('role', 'promoter')->update(['revoked_at' => null]);
|
|
$doc = DB::table('documents')->where('id', $nda['document_id'])->first();
|
|
Storage::disk('local')->put($doc->storage_key, Crypt::encryptString('TAMPERED SYNTHETIC BYTES'));
|
|
$this->getJson($url.'/confidencial')->assertForbidden();
|
|
$this->getJson($url.'/confidencialidade')->assertOk()->assertJsonPath('nda_pending', true);
|
|
$this->assertNull($record->fresh()->published_at);
|
|
}
|
|
|
|
public function test_compiled_confidential_page_has_no_company_data_before_both_conditions(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner, details: ['business' => ['reason' => 'PRIVATE-HTML-CANARY']]);
|
|
$this->approve($owner, $record);
|
|
$directory = getenv('EDITORIAL_QA_BUILD_DIRECTORY') ?: 'build';
|
|
$manifest = json_decode(file_get_contents(public_path($directory.'/manifest.json')), true, 512, JSON_THROW_ON_ERROR);
|
|
$this->app->instance(Vite::class, (new Vite)->useBuildDirectory($directory)->useHotFile(storage_path('phpunit-offers-no-hot')));
|
|
$entry = 'resources/js/pages/offers/confidentiality.tsx';
|
|
$this->assertArrayHasKey($entry, $manifest);
|
|
$this->login($this->actor())->get(self::BASE.'/'.$record->id.'/confidencialidade', ['Accept' => 'text/html'])->assertOk()
|
|
->assertHeader('Cache-Control', 'no-store, private')->assertSee($manifest[$entry]['file'], false)->assertDontSee('PRIVATE-HTML-CANARY')->assertDontSee('PRIVATE-CONTACT-CANARY');
|
|
}
|
|
|
|
public function test_photo_metadata_and_original_are_bound_to_the_editorial_fingerprint(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$resourceId = $this->upload($owner, $record, 'images', $this->png('metadata.png', 400, 400))->assertCreated()->json('id');
|
|
$resource = DB::table('opportunity_images')->where('id', $resourceId)->first();
|
|
(new ScanDocumentForMalware($resource->document_id))->handle(new CleanScannerFixture);
|
|
$before = app(PrivateOfferPreparation::class)->snapshot($record->fresh());
|
|
$this->postJson(self::BASE.'/'.$record->id.'/recursos/images/'.$resourceId.'/guardar', ['etag' => $this->etag($record), 'version' => 1,
|
|
'payload' => ['visibility' => 'internal', 'caption' => 'Changed synthetic caption', 'digitally_edited' => true, 'edit_description' => 'Synthetic QA declared edit'], 'reason' => self::REASON])->assertOk();
|
|
$after = app(PrivateOfferPreparation::class)->snapshot($record->fresh());
|
|
$this->assertNotSame($before['hash'], $after['hash']);
|
|
$this->assertSame(1, $before['resources']['images'][$resourceId]['version']);
|
|
$this->assertSame(2, $after['resources']['images'][$resourceId]['version']);
|
|
$this->assertSame($resource->document_id, $after['resources']['images'][$resourceId]['document_id']);
|
|
$this->assertTrue($after['resources']['images'][$resourceId]['payload']['digitally_edited']);
|
|
$this->approve($owner, $record);
|
|
DB::table('documents')->where('id', $resource->document_id)->update(['status' => 'quarantined', 'malware_scan_status' => 'pending']);
|
|
$this->login($owner)->getJson(self::BASE.'/'.$record->id)->assertOk()->assertJsonPath('record.approvalFresh', false);
|
|
$this->postJson(self::BASE.'/'.$record->id.'/revisao', ['etag' => $this->etag($record), 'action' => 'request_revision', 'reason' => self::REASON])->assertOk()->assertJsonPath('record.state', 'needs_information')->assertJsonPath('record.canEdit', true);
|
|
}
|
|
}
|