298 lines
22 KiB
PHP
298 lines
22 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\Sector;
|
||
use App\Models\User;
|
||
use App\Services\ProactiveSupply;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Str;
|
||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||
use Tests\TestCase;
|
||
|
||
final class ProtectedSupplyAggregateTest extends TestCase
|
||
{
|
||
use DatabaseTransactions;
|
||
|
||
private User $owner;
|
||
|
||
private array $geo = [];
|
||
|
||
private array $sectors = [];
|
||
|
||
private array $activities = [];
|
||
|
||
private int $observationSequence = 0;
|
||
|
||
private string $sourceHash;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
$db = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||
$this->assertSame('sector_context_20261115_test', $db->name);
|
||
$this->assertSame(55441, (int) $db->port);
|
||
$this->withoutVite();
|
||
config(['portal.profile_access_enabled' => true, 'investor-path.enabled' => true, 'investor-path.protected_supply_enabled' => true, 'investor-path.aggregate_supply_qa_enabled' => false]);
|
||
$this->owner = User::factory()->create();
|
||
foreach (['country' => [null, 'country'], 'province' => ['country', 'province'], 'other_province' => ['country', 'province'], 'small' => ['province', 'district'], 'large' => ['province', 'district'], 'post' => ['large', 'admin_post'], 'locality_a' => ['post', 'locality'], 'locality_b' => ['post', 'locality']] as $key => [$parent, $type]) {
|
||
$id = (string) Str::uuid();
|
||
$this->geo[$key] = $id;
|
||
DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent ? $this->geo[$parent] : null, 'name' => 'SYNTHETIC '.$key, 'code' => $id,
|
||
'location_type' => $type, 'source_system' => 'cartografia-workbook', 'data_classification' => 'public',
|
||
'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id]);
|
||
}
|
||
$catalog = (string) Str::uuid();
|
||
DB::table('reference_catalog_versions')->insert(['id' => $catalog, 'catalog_code' => 'qa-'.Str::random(16), 'version_no' => 1, 'source_system' => 'SYNTHETIC', 'source_version' => 'SYNTHETIC', 'source_reference' => 'SYNTHETIC', 'status' => 'published']);
|
||
foreach (['agro' => ['A', '01'], 'hotel' => ['I', '55'], 'infrastructure' => ['F', '41'], 'mining' => ['B', '07'], 'energy' => ['D', '35']] as $key => [$section, $division]) {
|
||
$sector = Sector::create(['name' => 'SYNTHETIC '.$key, 'slug' => 'qa-'.Str::lower(Str::random(16)), 'is_active' => true]);
|
||
$this->sectors[$key] = $sector->id;
|
||
$parent = null;
|
||
foreach ([$section, $division, $division.'110'] as $index => $code) {
|
||
$id = (string) Str::uuid();
|
||
DB::table('economic_activities')->insert(['id' => $id, 'parent_id' => $parent, 'sector_id' => $index === 0 ? $sector->id : null,
|
||
'catalog_version_id' => $catalog, 'code' => $code, 'label_pt_mz' => 'SYNTHETIC public '.$key.' '.$code,
|
||
'status' => 'published', 'valid_from' => '2020-01-01']);
|
||
$parent = $id;
|
||
}
|
||
$this->activities[$key] = $parent;
|
||
}
|
||
$source = hash('sha256', Str::uuid());
|
||
$this->sourceHash = $source;
|
||
DB::table('proactive_supply_sources')->insert(['sha256' => $source, 'name' => 'PRIVATE-ORIGINAL.xlsx', 'period' => '2020', 'limitations' => 'SYNTHETIC', 'receipt' => json_encode(['candidate' => 'a022ed9848b41ba342b4209414c2bbf2ebd0a7a3', 'access' => 'internal', 'synthetic' => true], JSON_THROW_ON_ERROR), 'imported_at' => now()]);
|
||
foreach (['01', '55', '41', '07', '35'] as $division) {
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$this->observation($source, $division, 'large-'.$i, $this->geo[$i < 3 ? 'locality_a' : 'locality_b']);
|
||
}
|
||
for ($i = 0; $i < 4; $i++) {
|
||
$this->observation($source, $division, 'small-'.$i, $this->geo['small']);
|
||
}
|
||
for ($i = 0; $i < 5; $i++) {
|
||
$this->observation($source, $division, 'other-'.$i, $this->geo['other_province']);
|
||
}
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$this->observation($source, $division, 'large-0', $this->geo['locality_a']);
|
||
}
|
||
}
|
||
}
|
||
|
||
private function observation(string $source, string $division, string $entity, string $territory): string
|
||
{
|
||
$id = hash('sha256', Str::uuid());
|
||
DB::table('proactive_supply_observations')->insert(['id' => $id, 'source_hash' => $source, 'sheet' => 'PRIVATE-SHEET', 'row_number' => ++$this->observationSequence,
|
||
'entity_key' => hash('sha256', $division.$entity), 'name' => 'PRIVATE-COMPANY-'.$entity, 'province' => 'PRIVATE', 'district' => 'PRIVATE',
|
||
'cae' => $division.'110', 'activity' => 'PRIVATE-CONTACT +258000000000', 'sector' => 'PRIVATE-RAW-SECTOR', 'source_date' => '2020', 'fingerprint' => $id,
|
||
'duplicate_of' => null, 'identity_conflict' => false, 'territory_id' => $territory, 'territory_resolution' => 'SYNTHETIC']);
|
||
|
||
return $id;
|
||
}
|
||
|
||
private function dossier(string $sector = 'agro', string $territory = 'locality_a'): string
|
||
{
|
||
$id = $this->actingAs($this->owner)->postJson('/investimentos', ['title' => 'SYNTHETIC deliberate '.$sector, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
|
||
$v = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json('case.version');
|
||
$body = ['version' => $v, 'selection' => ['sector' => 'sector:'.$this->sectors[$sector], 'territory' => 'territory:'.$this->geo[$territory]],
|
||
'opportunity_choices' => [], 'declarations' => ['project_title' => 'SYNTHETIC deliberate '.$sector, 'products_services' => 'SYNTHETIC deliberate activity', 'currency' => 'MZN', 'investment_budget_min' => '1000000', 'investment_budget_max' => '5000000']];
|
||
$hash = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json('preview_hash');
|
||
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'preview_hash' => $hash, 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk();
|
||
|
||
return $id;
|
||
}
|
||
|
||
private function aggregateSnapshot(string $id, array $extra = []): array
|
||
{
|
||
$version = $this->getJson('/investimentos/'.$id)->assertOk()->json('case.version');
|
||
|
||
return $this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $version, 'operation_key' => (string) Str::uuid(), ...$extra])->assertOk()->json('analysis');
|
||
}
|
||
|
||
public function test_five_sector_dossiers_save_recover_and_filter_only_protected_statistics(): void
|
||
{
|
||
$assignments = DB::table('portal_role_assignments')->count();
|
||
foreach (array_keys($this->sectors) as $sector) {
|
||
$id = $this->dossier($sector);
|
||
$a = $this->aggregateSnapshot($id, ['activity_id' => $this->activities[$sector]]);
|
||
$this->assertSame(1, $a['privacy_version']);
|
||
$this->assertNull($a['entities']);
|
||
$this->assertNull($a['repeated']);
|
||
$this->assertNull($a['uncertain']);
|
||
$this->assertCount(1, $a['groups']);
|
||
$this->assertSame('5–9', $a['groups'][0]['entities_band']);
|
||
$this->assertSame('10–14', $a['groups'][0]['observations_band']);
|
||
$this->assertNull($a['groups'][0]['total']);
|
||
$this->assertSame($this->geo['large'], $a['groups'][0]['scope']['id']);
|
||
$this->assertTrue($a['broader_context']);
|
||
$this->assertSame('2020', $a['source_summary']['period']);
|
||
$this->assertSame($this->activities[$sector], $a['filters']['activity_id']);
|
||
$this->assertStringNotContainsString('PRIVATE-', json_encode($a, JSON_THROW_ON_ERROR));
|
||
$v = $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonCount(1, 'supply_aggregates')->assertJsonPath('supply_aggregates.0.privacy_version', 1)->json('case.version');
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $v - 1, 'operation_key' => $a['operation_key'], 'activity_id' => $this->activities[$sector]])->assertOk()->assertJsonPath('replayed', true);
|
||
$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('prefill.simulator.investment_budget_min', '1000000')->assertJsonPath('prefill.simulator.investment_budget_max', '5000000')->assertJsonPath('prefill.simulator.project_title', 'SYNTHETIC deliberate '.$sector);
|
||
}
|
||
$this->assertSame($assignments, DB::table('portal_role_assignments')->count());
|
||
}
|
||
|
||
public function test_small_cells_and_differencing_do_not_reveal_exact_counts(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$service = app(ProactiveSupply::class);
|
||
$fields = ['sector_id' => $this->sectors['agro']];
|
||
$before = DB::selectOne("select NULLIF(current_setting('app.actor_user_id',true),'') as actor,NULLIF(current_setting('app.proactive_supply_read',true),'') as scope");
|
||
$local = $service->protectedAggregate($this->owner, $id, $this->geo['locality_a'], $fields);
|
||
$otherLocal = $service->protectedAggregate($this->owner, $id, $this->geo['locality_b'], $fields);
|
||
$district = $service->protectedAggregate($this->owner, $id, $this->geo['large'], $fields);
|
||
$province = $service->protectedAggregate($this->owner, $id, $this->geo['province'], $fields);
|
||
$small = $service->protectedAggregate($this->owner, $id, $this->geo['small'], $fields);
|
||
$this->assertSame($local['groups'], $otherLocal['groups']);
|
||
$this->assertSame($local['groups'], $district['groups']);
|
||
$this->assertSame($district['groups'], $province['groups']);
|
||
$this->assertSame([], $small['groups']);
|
||
$this->assertNull($province['entities']);
|
||
$this->assertNull($district['groups'][0]['total']);
|
||
$this->assertSame((array) $before, (array) DB::selectOne("select NULLIF(current_setting('app.actor_user_id',true),'') as actor,NULLIF(current_setting('app.proactive_supply_read',true),'') as scope"));
|
||
}
|
||
|
||
public function test_owner_isolation_no_raw_access_unknown_filter_rejection_and_disabled_guard(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$version = $this->getJson('/investimentos/'.$id)->json('case.version');
|
||
$body = ['version' => $version, 'operation_key' => (string) Str::uuid()];
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', [...$body, 'territory_id' => $this->geo['other_province']])->assertUnprocessable();
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', [...$body, 'activity_id' => $this->activities['hotel']])->assertUnprocessable();
|
||
$this->getJson('/minhas-oportunidades/oferta-interna')->assertForbidden();
|
||
try {
|
||
app(ProactiveSupply::class)->authorize($this->owner);
|
||
$this->fail('Aggregate capability must not grant internal source access.');
|
||
} catch (HttpException $error) {
|
||
$this->assertSame(403, $error->getStatusCode());
|
||
}
|
||
$this->actingAs(User::factory()->create())->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', $body)->assertForbidden();
|
||
$this->getJson('/investimentos/'.$id)->assertForbidden();
|
||
$this->actingAs($this->owner);
|
||
$a = $this->aggregateSnapshot($id);
|
||
config(['investor-path.protected_supply_enabled' => false]);
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', $body)->assertNotFound();
|
||
$this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('supply_aggregates', [])->assertJsonPath('supply_aggregate_enabled', false);
|
||
config(['investor-path.protected_supply_enabled' => true]);
|
||
$this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('supply_aggregates.0.operation_key', $a['operation_key']);
|
||
}
|
||
|
||
public function test_no_raw_export_no_guest_access_and_no_legacy_exact_history_disclosure(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$a = $this->aggregateSnapshot($id, ['activity_id' => $this->activities['agro']]);
|
||
$version = $this->getJson('/investimentos/'.$id)->json('case.version');
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $version, 'operation_key' => $a['operation_key'], 'activity_id' => null])->assertConflict();
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $version, 'operation_key' => (string) Str::uuid(), 'export' => 'csv'])->assertUnprocessable();
|
||
$this->getJson('/investimentos/'.$id.'/contexto/oferta-agregada?export=csv')->assertStatus(405);
|
||
$row = [...$a, 'PRIVATE-RAW' => 'PRIVATE-COMPANY', 'entities' => 1];
|
||
$row['groups'][0]['total'] = 1;
|
||
$row['groups'][0]['PRIVATE-CONTACT'] = 'PRIVATE-CONTACT';
|
||
$safe = app(ProactiveSupply::class)->protectedHistory([[...$a, 'privacy_version' => 0], $row]);
|
||
$this->assertCount(1, $safe);
|
||
$this->assertNull($safe[0]['entities']);
|
||
$this->assertNull($safe[0]['groups'][0]['total']);
|
||
$this->assertStringNotContainsString('PRIVATE-', json_encode($safe, JSON_THROW_ON_ERROR));
|
||
$row['groups'][0]['entities_band'] = '1–1';
|
||
$this->assertSame([], app(ProactiveSupply::class)->protectedHistory([$row])[0]['groups']);
|
||
$this->app['auth']->forgetGuards();
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $version, 'operation_key' => (string) Str::uuid()])->assertUnauthorized();
|
||
}
|
||
|
||
public function test_unpublished_classification_is_not_presented_as_an_empty_business_result(): void
|
||
{
|
||
$sector = Sector::create(['name' => 'SYNTHETIC without public CAE', 'slug' => 'qa-'.Str::lower(Str::random(16)), 'is_active' => true]);
|
||
$this->sectors['without_cae'] = $sector->id;
|
||
$catalog = (string) Str::uuid();
|
||
DB::table('reference_catalog_versions')->insert(['id' => $catalog, 'catalog_code' => 'qa-'.Str::random(16), 'version_no' => 1, 'source_system' => 'SYNTHETIC', 'source_version' => 'SYNTHETIC', 'source_reference' => 'SYNTHETIC', 'status' => 'draft']);
|
||
DB::table('economic_activities')->insert(['id' => (string) Str::uuid(), 'sector_id' => $sector->id, 'catalog_version_id' => $catalog, 'code' => '01', 'label_pt_mz' => 'SYNTHETIC unpublished division', 'status' => 'draft', 'valid_from' => '2020-01-01']);
|
||
$id = $this->dossier('without_cae', 'locality_a');
|
||
$before = $this->actingAs($this->owner)->getJson('/investimentos/'.$id)->assertOk()->json();
|
||
$response = $this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $before['case']['version'], 'operation_key' => (string) Str::uuid(), 'activity_id' => null])->assertUnprocessable();
|
||
$this->assertStringContainsString('não significa ausência de empresas', $response->json('message'));
|
||
$after = $this->getJson('/investimentos/'.$id)->assertOk()->json();
|
||
$this->assertSame($before['case']['version'], $after['case']['version']);
|
||
$this->assertSame([], $after['supply_aggregates']);
|
||
$this->assertContains('ebau:979', array_column($after['supply_activity_options'], 'id'));
|
||
}
|
||
|
||
public function test_accepted_source_classification_works_by_code_or_exact_label_without_public_numeric_catalogue(): void
|
||
{
|
||
$sector = Sector::create(['name' => 'SYNTHETIC general tourism', 'slug' => 'qa-'.Str::lower(Str::random(16)), 'is_active' => true]);
|
||
$this->sectors['source_only'] = $sector->id;
|
||
$id = $this->dossier('source_only');
|
||
DB::table('proactive_supply_observations')->where('source_hash', $this->sourceHash)->where('cae', '55110')->update(['cae' => '55111', 'activity' => 'HOTÉIS COM RESTAURANTE']);
|
||
$coded = $this->aggregateSnapshot($id, ['classification_key' => 'ebau:979']);
|
||
$this->assertSame('55', $coded['groups'][0]['code']);
|
||
$this->assertSame('5–9', $coded['groups'][0]['entities_band']);
|
||
$this->assertSame('ebau:979', $coded['filters']['classification_key']);
|
||
$this->assertSame($sector->id, $coded['filters']['sector_id']);
|
||
DB::table('proactive_supply_observations')->where('source_hash', $this->sourceHash)->where('cae', '55111')->update(['cae' => '']);
|
||
$labelled = $this->aggregateSnapshot($id, ['classification_key' => 'ebau:979']);
|
||
$this->assertSame($coded['groups'], $labelled['groups']);
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$row = $this->observation($this->sourceHash, '56', 'restaurant-'.$i, $this->geo['large']);
|
||
DB::table('proactive_supply_observations')->where('id', $row)->update(['cae' => '', 'activity' => 'RESTAURANTES (INCLUI ACTIVIDADES DE RESTAURAÇÃO EM MEIOS MÓVEIS)']);
|
||
}
|
||
$restaurant = $this->aggregateSnapshot($id, ['classification_key' => 'ebau:345']);
|
||
$this->assertSame('56', $restaurant['groups'][0]['code']);
|
||
$this->assertSame('5–9', $restaurant['groups'][0]['entities_band']);
|
||
$after = $this->getJson('/investimentos/'.$id)->assertOk()->json();
|
||
$this->assertCount(3, $after['supply_aggregates']);
|
||
$this->assertSame('ebau:345', $after['supply_aggregates'][2]['filters']['classification_key']);
|
||
$this->assertStringNotContainsString('PRIVATE-', json_encode($after['supply_aggregates'], JSON_THROW_ON_ERROR));
|
||
$this->assertSame('1000000', $after['investment_context']['fields']['investment_budget_min']);
|
||
}
|
||
|
||
public function test_conflicting_and_unidentified_source_classifications_are_not_invented_matches(): void
|
||
{
|
||
$id = $this->dossier('hotel');
|
||
DB::table('proactive_supply_observations')->where('source_hash', $this->sourceHash)->where('cae', '55110')->update(['cae' => '01', 'activity' => 'HOTÉIS COM RESTAURANTE']);
|
||
$a = $this->aggregateSnapshot($id, ['classification_key' => 'ebau:979']);
|
||
$this->assertSame([], $a['groups']);
|
||
$v = $this->getJson('/investimentos/'.$id)->json('case.version');
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $v, 'operation_key' => (string) Str::uuid(), 'classification_key' => 'ebau:99999999'])->assertUnprocessable();
|
||
$this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $v, 'operation_key' => (string) Str::uuid(), 'classification_key' => 'ebau:979', 'activity_id' => $this->activities['hotel']])->assertUnprocessable();
|
||
$this->actingAs(User::factory()->create())->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $v, 'operation_key' => (string) Str::uuid(), 'classification_key' => 'ebau:979'])->assertForbidden();
|
||
}
|
||
|
||
public function test_previously_accepted_tourism_scope_is_accommodation_without_inventing_a_specific_subtype(): void
|
||
{
|
||
$source = 'f02e086ed4973ef17234aa6cc54b98fbb7a6952ffb16ec4dc4f7d0c72af6400c';
|
||
$this->assertFalse(DB::table('proactive_supply_sources')->where('sha256', $source)->exists(), 'Only isolated synthetic source allowed');
|
||
DB::table('proactive_supply_sources')->insert(['sha256' => $source, 'name' => 'SYNTHETIC accepted tourism contract', 'period' => '2024', 'limitations' => 'SYNTHETIC', 'receipt' => json_encode(['candidate' => 'a022ed9848b41ba342b4209414c2bbf2ebd0a7a3', 'access' => 'internal', 'synthetic' => true], JSON_THROW_ON_ERROR), 'imported_at' => now()]);
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$row = $this->observation($source, '55', 'tourism-broad-'.$i, $this->geo['large']);
|
||
DB::table('proactive_supply_observations')->where('id', $row)->update(['cae' => '', 'activity' => 'Alojamento (âmbito do exemplar; categoria por conferir)']);
|
||
}
|
||
$id = $this->dossier('hotel');
|
||
$a = $this->aggregateSnapshot($id, ['classification_key' => 'ebau:158']);
|
||
$this->assertSame('ALOJAMENTO', $a['groups'][0]['label']);
|
||
$this->assertSame('5–9', $a['groups'][0]['entities_band']);
|
||
$this->assertStringNotContainsString('HOTÉIS COM RESTAURANTE', $a['groups'][0]['label']);
|
||
}
|
||
|
||
public function test_unresolved_observation_keys_conflicts_duplicates_and_unknown_import_contracts_do_not_form_a_publishable_cohort(): void
|
||
{
|
||
$unknown = hash('sha256', Str::uuid());
|
||
DB::table('proactive_supply_sources')->insert(['sha256' => $unknown, 'name' => 'PRIVATE-FUTURE-SOURCE', 'period' => '2025', 'limitations' => 'SYNTHETIC', 'receipt' => json_encode(['candidate' => 'UNKNOWN', 'access' => 'internal'], JSON_THROW_ON_ERROR), 'imported_at' => now()]);
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$id = $this->observation($this->sourceHash, '01', 'unresolved-'.$i, $this->geo['small']);
|
||
$unresolved = hash('sha256', 'unresolved:'.$this->sourceHash.'|PRIVATE-SHEET|'.$this->observationSequence);
|
||
DB::table('proactive_supply_observations')->where('id', $id)->update(['entity_key' => $unresolved]);
|
||
$conflict = $this->observation($this->sourceHash, '01', 'conflict-'.$i, $this->geo['small']);
|
||
DB::table('proactive_supply_observations')->where('id', $conflict)->update(['identity_conflict' => true]);
|
||
$duplicate = $this->observation($this->sourceHash, '01', 'duplicate-'.$i, $this->geo['small']);
|
||
DB::table('proactive_supply_observations')->where('id', $duplicate)->update(['duplicate_of' => $id]);
|
||
$this->observation($unknown, '01', 'unknown-'.$i, $this->geo['small']);
|
||
}
|
||
$id = $this->dossier('agro', 'small');
|
||
$a = $this->aggregateSnapshot($id);
|
||
$this->assertSame([], $a['groups']);
|
||
$this->assertNull($a['entities']);
|
||
$this->assertSame('2020', $a['source_summary']['period']);
|
||
$this->assertStringNotContainsString('PRIVATE-', json_encode($a, JSON_THROW_ON_ERROR));
|
||
}
|
||
}
|