Files
coolify/tests/Feature/AuditableHeartbeatFieldsTest.php
Andras BacsaiandClaude Opus 5.5 9862f75dc2 fix(audit): reduce audit log noise and keep instance events out of team logs
- Ignore status, heartbeat, and check fields that background checks write
  during a user request.
- Record repeated unauthenticated failures (failed logins, API auth,
  webhook signatures) once per source in a short window.
- An explicit null team_id in the context is kept, so instance events are
  not stored on the admin's current team.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:01:10 +02:00

133 lines
5.0 KiB
PHP

<?php
use App\Models\Application;
use App\Models\AuditEvent;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\StandaloneSqlite;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Once;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->withoutDefer();
InstanceSettings::forceCreate(['id' => 0]);
Once::flush();
Server::flushIdentityMap();
Log::spy();
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
$project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $project->id]);
});
function createAuditedResourceForHeartbeatTest(string $model, Environment $environment): Model
{
if ($model === Application::class) {
return Application::factory()->create(['environment_id' => $environment->id]);
}
$passwords = [
StandalonePostgresql::class => ['postgres_password' => 'review-fix-e-password'],
StandaloneMysql::class => ['mysql_root_password' => 'review-fix-e-password', 'mysql_password' => 'review-fix-e-password'],
StandaloneMariadb::class => ['mariadb_root_password' => 'review-fix-e-password', 'mariadb_password' => 'review-fix-e-password'],
StandaloneMongodb::class => ['mongo_initdb_root_password' => 'review-fix-e-password'],
StandaloneRedis::class => [],
StandaloneKeydb::class => ['keydb_password' => 'review-fix-e-password'],
StandaloneDragonfly::class => ['dragonfly_password' => 'review-fix-e-password'],
StandaloneClickhouse::class => ['clickhouse_admin_password' => 'review-fix-e-password'],
StandaloneSqlite::class => [],
];
return $model::create([
'uuid' => fake()->uuid(),
'name' => 'review-fix-e-database',
'status' => 'exited',
'environment_id' => $environment->id,
'destination_type' => Server::class,
'destination_id' => 0,
...$passwords[$model],
]);
}
test('container heartbeat and restart tracking writes are not audited as user changes', function (string $model) {
$resource = createAuditedResourceForHeartbeatTest($model, $this->environment);
AuditEvent::query()->delete();
$resource->update(['status' => 'running:healthy', 'last_online_at' => now()]);
$resource->update(['restart_count' => 3, 'last_restart_at' => now(), 'last_restart_type' => 'crash']);
$resource->update(['config_hash' => 'review-fix-e-hash']);
$resource->update($model === Application::class
? ['container_present' => false, 'restart_limit_reached' => true]
: ['started_at' => now()]);
expect(AuditEvent::query()->count())->toBe(0);
$resource->update(['name' => 'review-fix-e-renamed', 'restart_count' => 0]);
expect(AuditEvent::query()->sole()->metadata['changed_fields'])->toBe(['name']);
})->with([
Application::class,
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneRedis::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
StandaloneSqlite::class,
]);
test('application dns check results are not audited as user changes', function () {
$application = Application::factory()->create(['environment_id' => $this->environment->id]);
AuditEvent::query()->delete();
$application->update(['domain_dns_statuses' => ['https://example.com' => ['status' => 'ok']], 'custom_healthcheck_found' => true]);
expect(AuditEvent::query()->count())->toBe(0);
});
test('server heartbeat and check state writes are not audited as user changes', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
AuditEvent::query()->delete();
$server->sentinelHeartbeat();
$server->update([
'sentinel_waiting_since' => now(),
'unreachable_count' => 2,
'high_disk_usage_notification_sent' => true,
'log_drain_notification_sent' => true,
'validation_logs' => 'review-fix-e-validation',
'is_validating' => true,
'detected_traefik_version' => 'v3.1.0',
'traefik_outdated_info' => ['current' => 'v3.1.0'],
]);
expect(AuditEvent::query()->count())->toBe(0);
$server->update(['description' => 'review-fix-e-description']);
expect(AuditEvent::query()->sole()->metadata['changed_fields'])->toBe(['description']);
});