@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user