Spec 094: Assignment ops observability hardening #113
@ -55,6 +55,9 @@
|
|||||||
"@php artisan config:clear --ansi",
|
"@php artisan config:clear --ansi",
|
||||||
"@php artisan test"
|
"@php artisan test"
|
||||||
],
|
],
|
||||||
|
"test:pgsql": [
|
||||||
|
"@php vendor/bin/pest -c phpunit.pgsql.xml"
|
||||||
|
],
|
||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
"@php artisan package:discover --ansi",
|
"@php artisan package:discover --ansi",
|
||||||
|
|||||||
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function tenantOwnedTables(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'policies',
|
||||||
|
'policy_versions',
|
||||||
|
'backup_sets',
|
||||||
|
'backup_items',
|
||||||
|
'restore_runs',
|
||||||
|
'backup_schedules',
|
||||||
|
'inventory_items',
|
||||||
|
'inventory_links',
|
||||||
|
'entra_groups',
|
||||||
|
'findings',
|
||||||
|
'entra_role_definitions',
|
||||||
|
'tenant_permissions',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureWorkspaceForeignKey(string $tableName, string $constraintName): void
|
||||||
|
{
|
||||||
|
// Add as NOT VALID (fast) then VALIDATE (safe) — Postgres best practice.
|
||||||
|
DB::unprepared(<<<SQL
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = '{$constraintName}'
|
||||||
|
) THEN
|
||||||
|
EXECUTE format(
|
||||||
|
'ALTER TABLE %I ADD CONSTRAINT %I FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE NOT VALID',
|
||||||
|
'{$tableName}',
|
||||||
|
'{$constraintName}'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = '{$constraintName}'
|
||||||
|
AND convalidated = false
|
||||||
|
) THEN
|
||||||
|
EXECUTE format(
|
||||||
|
'ALTER TABLE %I VALIDATE CONSTRAINT %I',
|
||||||
|
'{$tableName}',
|
||||||
|
'{$constraintName}'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (DB::getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->tenantOwnedTables() as $tableName) {
|
||||||
|
$constraintName = sprintf('%s_workspace_fk', $tableName);
|
||||||
|
|
||||||
|
$this->ensureWorkspaceForeignKey($tableName, $constraintName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (DB::getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->tenantOwnedTables() as $tableName) {
|
||||||
|
$constraintName = sprintf('%s_workspace_fk', $tableName);
|
||||||
|
|
||||||
|
DB::unprepared(<<<SQL
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
EXECUTE format(
|
||||||
|
'ALTER TABLE %I DROP CONSTRAINT IF EXISTS %I',
|
||||||
|
'{$tableName}',
|
||||||
|
'{$constraintName}'
|
||||||
|
);
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
50
phpunit.pgsql.xml
Normal file
50
phpunit.pgsql.xml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
|
||||||
|
<php>
|
||||||
|
<ini name="memory_limit" value="2048M"/>
|
||||||
|
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_KEY" value="base64:z63PQuXp3rUOQ0L4o8xp76xeakrn5X3owja1qFX3ccY="/>
|
||||||
|
<env name="INTUNE_TENANT_ID" value="" force="true"/>
|
||||||
|
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
|
||||||
|
<!-- Postgres-backed test configuration (for schema-level assertions, FK validation, etc.) -->
|
||||||
|
<env name="DB_CONNECTION" value="pgsql"/>
|
||||||
|
<env name="DB_HOST" value="pgsql"/>
|
||||||
|
<env name="DB_PORT" value="5432"/>
|
||||||
|
<env name="DB_DATABASE" value="tenantatlas_testing"/>
|
||||||
|
<env name="DB_USERNAME" value="root"/>
|
||||||
|
<env name="DB_PASSWORD" value="postgres"/>
|
||||||
|
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
it('enforces workspace_id foreign keys on tenant-owned tables', function () {
|
||||||
|
if (DB::getDriverName() !== 'pgsql') {
|
||||||
|
$this->markTestSkipped('Postgres-only: validates FK constraints via pg_constraint.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = [
|
||||||
|
'policies',
|
||||||
|
'policy_versions',
|
||||||
|
'backup_sets',
|
||||||
|
'backup_items',
|
||||||
|
'restore_runs',
|
||||||
|
'backup_schedules',
|
||||||
|
'inventory_items',
|
||||||
|
'inventory_links',
|
||||||
|
'entra_groups',
|
||||||
|
'findings',
|
||||||
|
'entra_role_definitions',
|
||||||
|
'tenant_permissions',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$sql = <<<'SQL'
|
||||||
|
SELECT c.conname, c.convalidated
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_class rel ON rel.oid = c.conrelid
|
||||||
|
JOIN pg_class ref ON ref.oid = c.confrelid
|
||||||
|
JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = ANY(c.conkey)
|
||||||
|
WHERE c.contype = 'f'
|
||||||
|
AND rel.relname = ?
|
||||||
|
AND ref.relname = 'workspaces'
|
||||||
|
AND att.attname = 'workspace_id'
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
$constraints = DB::select(
|
||||||
|
$sql,
|
||||||
|
[$table],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($constraints)->not->toBeEmpty();
|
||||||
|
|
||||||
|
$allValidated = collect($constraints)->every(fn ($c): bool => (bool) $c->convalidated);
|
||||||
|
expect($allValidated)->toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user