commit 30/9
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-09-30 09:05:43 +02:00
parent 67dad461d9
commit 3dc7c5c649
75 changed files with 4123 additions and 107 deletions
@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\ExistingBusinessDrafts;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ExistingBusinessCompatibilityTest extends TestCase
{
use RefreshDatabase;
public function test_shadow_unavailability_does_not_change_private_contract_or_authorization(): void
{
config(['existing-business.enabled' => true, 'existing-business.compatibility_shadow' => true]);
Queue::fake();
$owner = User::factory()->create();
$other = User::factory()->create();
$business = app(ExistingBusinessDrafts::class)->save($owner, ['modality' => 'company_sale',
'classification' => 'confidential', 'reason' => 'Synthetic compatibility case', 'payload' => ['title' => 'Reserved synthetic title']]);
$url = '/api/v1/existing-business/listings/'.$business->id;
$this->actingAs($owner)->getJson($url)->assertOk()->assertJsonPath('record.payload.title', 'Reserved synthetic title');
$this->actingAs($other)->getJson($url)->assertForbidden();
Queue::assertNothingPushed();
$this->assertDatabaseCount('existing_business_revisions', 1);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class ExistingBusinessWizardTest extends TestCase
{
use RefreshDatabase;
public function test_sector_migration_reverses_empty_and_refuses_data_loss(): void
{
$migration = require database_path('migrations/2026_09_29_200000_existing_business_form_sector.php');
$migration->down();
$this->assertFalse(Schema::hasColumn('existing_businesses', 'sector_id'));
$migration->up();
$this->assertTrue(Schema::hasColumn('existing_businesses', 'sector_id'));
config(['existing-business.enabled' => true]);
$sector = DB::table('sectors')->insertGetId(['name' => 'Synthetic', 'slug' => 'rollback-synthetic', 'is_active' => true]);
$this->actingAs(User::factory()->create())->postJson('/api/v1/existing-business/listings', [
'modality' => 'company_sale', 'classification' => 'confidential', 'reason' => 'Synthetic rollback check',
'sector_id' => $sector, 'payload' => ['title' => 'Synthetic rollback'],
])->assertCreated();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Sector associations exist');
$migration->down();
}
public function test_draft_resume_validation_concurrency_confirmation_and_private_submission(): void
{
config(['existing-business.enabled' => true]);
$owner = User::factory()->create();
$sector = DB::table('sectors')->insertGetId(['name' => 'Synthetic', 'slug' => 'wizard-synthetic', 'is_active' => true]);
$this->actingAs($owner);
$draft = ['modality' => 'company_sale', 'classification' => 'confidential', 'reason' => 'Synthetic form draft', 'payload' => ['title' => 'Synthetic wizard', 'wizard_version' => '1', 'wizard_step' => '1']];
$record = $this->postJson('/api/v1/existing-business/listings', $draft)->assertCreated()->json();
$url = '/api/v1/existing-business/listings/'.$record['id'];
$transition = ['action' => 'submitted', 'version_no' => 1, 'note' => 'Synthetic submission request', 'review_consent' => true, 'confirmation_code' => $record['code']];
$this->postJson($url.'/transitions', $transition)->assertUnprocessable();
$payload = [...$draft['payload'], 'wizard_step' => '8', 'proponent_type' => 'person', 'proponent_name' => 'Synthetic owner',
'tax_identifier' => 'SYNTHETIC', 'contact_email' => 'synthetic@example.test', 'country' => 'MZ', 'acting_capacity' => 'representative',
'declared_holder' => 'Synthetic holder', 'legitimacy_declaration' => 'yes', 'location_notes' => 'Synthetic private location',
'description' => 'Synthetic description long enough for review.', 'source_reference' => 'Synthetic source reference',
'valid_until' => today()->addMonth()->toDateString(), 'asking_value' => '1234.50', 'currency' => 'MZN', 'value_nature' => 'asking'];
$save = [...$draft, 'version_no' => 1, 'sector_id' => $sector, 'payload' => $payload];
$this->putJson($url, [...$save, 'payload' => [...$payload, 'equity_percentage' => '101']])->assertUnprocessable();
$this->putJson($url, $save)->assertOk()->assertJsonPath('version_no', 2);
$this->getJson($url)->assertOk()->assertJsonPath('record.payload.wizard_step', '8')->assertJsonPath('record.sector_id', $sector);
$this->putJson($url, $save)->assertConflict();
$this->postJson($url.'/transitions', [...$transition, 'version_no' => 2])->assertUnprocessable()->assertJsonValidationErrors('mandate_reference');
$payload['mandate_reference'] = 'Synthetic mandate reference';
$payload['mandate_valid_until'] = today()->addMonth()->toDateString();
$this->putJson($url, [...$save, 'version_no' => 2, 'payload' => $payload])->assertOk();
$this->putJson($url, [...$save, 'version_no' => 3, 'payload' => $payload, 'modality' => 'real_estate'])->assertOk();
$this->postJson($url.'/transitions', [...$transition, 'version_no' => 4])->assertUnprocessable()->assertJsonValidationErrors('objects');
$this->putJson($url, [...$save, 'version_no' => 4, 'payload' => $payload])->assertOk();
$this->postJson($url.'/transitions', [...$transition, 'version_no' => 5, 'confirmation_code' => 'wrong'])->assertUnprocessable();
$this->postJson($url.'/transitions', [...$transition, 'version_no' => 5])->assertOk()->assertJsonPath('status', 'submitted');
$this->postJson($url.'/transitions', [...$transition, 'version_no' => 5])->assertConflict();
$this->actingAs(User::factory()->create())->getJson($url)->assertForbidden();
$this->assertDatabaseCount('existing_business_reviews', 1);
$raw = DB::table('existing_businesses')->where('id', $record['id'])->value('payload');
$this->assertStringNotContainsString('synthetic@example.test', $raw);
}
}
+7
View File
@@ -63,6 +63,10 @@ class InvestmentZoneTest extends TestCase
public function test_rollback_refuses_populated_foundation_and_empty_rollback_is_reversible(): void
{
$skillsMigrations = glob(database_path('migrations/2026_09_30_*_skills_education_*.php'));
foreach (array_reverse($skillsMigrations) as $file) {
(require $file)->down();
}
$migration = require database_path('migrations/2026_09_28_100000_investment_zone_foundation.php');
$originMigration = require database_path('migrations/2026_09_28_120000_investment_submission_zones.php');
$geometryMigration = require database_path('migrations/2026_09_28_110000_zone_geometry_review.php');
@@ -78,6 +82,9 @@ class InvestmentZoneTest extends TestCase
$originMigration->up();
$businessMigration->up();
$intakeMigration->up();
foreach ($skillsMigrations as $file) {
(require $file)->up();
}
$this->assertTrue(Schema::hasTable('investment_zone_links'));
$this->actingAs($this->manager())->postJson('/api/v2/investment/admin/zones', ['code' => 'QA', 'name' => 'Synthetic', 'category' => 'pending'])->assertCreated();
try {
@@ -0,0 +1,69 @@
<?php
namespace Tests\Feature;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class SkillsEducationSchemaTest extends TestCase
{
use RefreshDatabase;
private function rejected(string $sql, string $state): void
{
DB::statement('SAVEPOINT invalid_skills_case');
try {
DB::statement($sql);
$this->fail('Invalid write accepted');
} catch (QueryException $e) {
$this->assertSame($state, $e->getCode());
} finally {
DB::statement('ROLLBACK TO SAVEPOINT invalid_skills_case');
}
}
public function test_master_types_private_defaults_and_immutable_audit(): void
{
$this->assertSame(24, DB::table('pg_tables')->where('schemaname', 'skills_edu')->count());
$missing = DB::selectOne(<<<'SQL'
SELECT count(*) AS n FROM pg_constraint c JOIN pg_namespace ns ON ns.oid=c.connamespace
WHERE ns.nspname='skills_edu' AND c.contype='f' AND NOT EXISTS (
SELECT 1 FROM pg_index i JOIN pg_class ix ON ix.oid=i.indexrelid JOIN pg_am am ON am.oid=ix.relam
WHERE i.indrelid=c.conrelid AND i.indisvalid AND i.indisready AND i.indpred IS NULL
AND am.amname='btree' AND i.indnkeyatts>=cardinality(c.conkey)
AND ARRAY(SELECT x FROM unnest(i.indkey::smallint[]) WITH ORDINALITY k(x,p) WHERE p<=cardinality(c.conkey) ORDER BY p)=c.conkey)
SQL);
$this->assertSame(0, (int) $missing->n);
$this->assertNull(DB::selectOne("SELECT to_regclass('skills_edu.skills') AS id")->id);
$this->assertSame('bigint', DB::selectOne("SELECT format_type(atttypid,atttypmod) AS type FROM pg_attribute WHERE attrelid='skills_edu.professional_profiles'::regclass AND attname='user_master_id'")->type);
$this->rejected("INSERT INTO skills_edu.skill_details(id,category) VALUES(gen_random_uuid(),'technical')", '23503');
$this->rejected("INSERT INTO skills_edu.professional_profiles(full_name,registration_mode,user_master_id) VALUES('Synthetic','individual',9223372036854775807)", '23503');
$profile = DB::selectOne("INSERT INTO skills_edu.professional_profiles(full_name,registration_mode) VALUES('Synthetic','individual') RETURNING id,visibility_level,status");
$this->assertSame('private', $profile->visibility_level);
$this->assertSame('draft', $profile->status);
$this->rejected("INSERT INTO skills_edu.profile_experiences(profile_id,job_title,started_on,ended_on) VALUES('$profile->id','Synthetic','2026-02-02','2026-01-01')", '23514');
DB::statement("INSERT INTO skills_edu.audit_events(action,object_type,result,correlation_id) VALUES('synthetic','test','success',gen_random_uuid())");
foreach (['UPDATE skills_edu.audit_events SET action=action', 'DELETE FROM skills_edu.audit_events', 'TRUNCATE skills_edu.audit_events'] as $sql) {
$this->rejected($sql, '23514');
}
}
public function test_reverse_order_rollback_reapply_and_nonempty_refusal(): void
{
$files = glob(database_path('migrations/2026_09_30_*_skills_education_*.php'));
foreach (array_reverse($files) as $file) {
(require $file)->down();
}
$this->assertNull(DB::selectOne("SELECT to_regnamespace('skills_edu') AS id")->id);
foreach ($files as $file) {
(require $file)->up();
}
$this->assertSame(24, DB::table('pg_tables')->where('schemaname', 'skills_edu')->count());
DB::statement("INSERT INTO skills_edu.audit_events(action,object_type,result,correlation_id) VALUES('synthetic','test','success',gen_random_uuid())");
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('destructive rollback refused');
(require end($files))->down();
}
}
+19
View File
@@ -2,11 +2,30 @@
namespace Tests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\RefreshDatabaseState;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Laravel\Fortify\Features;
abstract class TestCase extends BaseTestCase
{
public function createApplication(): Application
{
$app = parent::createApplication();
// migrate:fresh only clears the connection's public search path. This module owns a separate schema.
if (! RefreshDatabaseState::$migrated && $app->environment('testing')) {
$db = $app->make('db')->connection();
if ($db->getDriverName() === 'pgsql'
&& in_array($db->getDatabaseName(), ['portal_services_test', 'investmoz_test'], true)
&& (string) $db->getConfig('port') === '55441'
&& in_array($db->getConfig('host'), ['127.0.0.1', 'localhost'], true)) {
$db->statement('DROP SCHEMA IF EXISTS skills_edu CASCADE');
}
}
return $app;
}
protected function setUp(): void
{
parent::setUp();