create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); DB::statement('SET app.current_tenant_id = ' . $tenant->id); // Ensure a saas_admin_users row exists for saas_admin_audit_log FK. // Must be is_active=true so incidents:watch-failures can resolve the FK too. $adminId = DB::table('saas_admin_users')->where('email', 'opflow-stub@test.local')->value('id'); if ($adminId === null) { $adminId = (int) DB::table('saas_admin_users')->insertGetId([ 'email' => 'opflow-stub@test.local', 'full_name' => 'OpFlow Test Stub', 'password_hash' => '$2y$04$system-stub-not-loginable', 'role' => 'super_admin', 'is_active' => true, 'sso_provider' => 'local', 'is_break_glass' => false, ]); } $adminId = (int) $adminId; // ── Step 1: Create a project → project.created in tenant_operations_log ── $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Опфлоу тест', 'signal_type' => 'site', 'signal_identifier' => 'opflow-test.ru', 'daily_limit_target' => 30, 'regions' => [], 'delivery_days_mask' => 127, ])->assertStatus(201); $createdProject = Project::where('tenant_id', $tenant->id) ->where('signal_identifier', 'opflow-test.ru') ->firstOrFail(); expect( DB::table('tenant_operations_log') ->where('tenant_id', $tenant->id) ->where('event', 'project.created') ->where('entity_id', $createdProject->id) ->exists() )->toBeTrue('project.created must be logged'); // ── Step 2: Update the project → project.updated in tenant_operations_log ─ $this->actingAs($user)->patchJson("/api/projects/{$createdProject->id}", [ 'daily_limit_target' => 60, ])->assertOk(); $updatedRow = DB::table('tenant_operations_log') ->where('tenant_id', $tenant->id) ->where('event', 'project.updated') ->where('entity_id', $createdProject->id) ->first(); expect($updatedRow)->not->toBeNull('project.updated must be logged'); $after = json_decode($updatedRow->payload_after, true); expect((int) $after['daily_limit_target'])->toBe(60); // ── Step 3: Regenerate API key → api_key.regenerated in tenant_operations_log $this->actingAs($user)->postJson('/api/api-keys/regenerate') ->assertStatus(201); $apiKeyRow = DB::table('tenant_operations_log') ->where('tenant_id', $tenant->id) ->where('event', 'api_key.regenerated') ->latest('id') ->first(); expect($apiKeyRow)->not->toBeNull('api_key.regenerated must be logged'); $keyPayload = json_decode($apiKeyRow->payload_after, true); expect($keyPayload['key_prefix'] ?? null)->not->toBeNull(); expect(strlen($keyPayload['key_prefix']))->toBeLessThan(20); expect(json_encode($keyPayload))->not->toContain('plain_key'); // ── Step 4: Change webhook URL → webhook_settings.updated in tenant_operations_log $oldUrl = 'https://8.8.8.8/hook'; OutboundWebhookSubscription::factory()->create([ 'tenant_id' => $tenant->id, 'user_id' => $user->id, 'target_url' => $oldUrl, ]); $newUrl = 'https://1.1.1.1/hook'; $this->actingAs($user)->putJson('/api/tenants/me/webhook-settings', [ 'target_url' => $newUrl, ])->assertOk(); $webhookSettingsRow = DB::table('tenant_operations_log') ->where('tenant_id', $tenant->id) ->where('event', 'webhook_settings.updated') ->latest('id') ->first(); expect($webhookSettingsRow)->not->toBeNull('webhook_settings.updated must be logged'); $wsAfter = json_decode($webhookSettingsRow->payload_after, true); $wsBefore = json_decode($webhookSettingsRow->payload_before, true); expect($wsAfter['target_url'] ?? null)->toBe($newUrl); expect($wsBefore['target_url'] ?? null)->toBe($oldUrl); // ── Step 5: Admin sets export mode → saas_admin_audit_log ──────────────── // Reset guards so Sanctum pollution from previous actingAs calls doesn't // interfere with the admin endpoint (which uses a different auth context). app('auth')->forgetGuards(); app('auth')->setDefaultDriver('web'); // Seed the system_settings key the admin endpoint needs DB::table('system_settings')->updateOrInsert( ['key' => 'supplier_export_mode'], ['value' => 'batch', 'type' => 'string', 'updated_at' => now()], ); $countBefore = DB::table('saas_admin_audit_log')->count(); $this->actingAs(User::factory()->create()) ->postJson('/api/admin/supplier-integration/export-mode', [ 'mode' => 'online', 'admin_user_id' => $adminId, ])->assertOk(); expect(DB::table('saas_admin_audit_log')->count())->toBe($countBefore + 1); $adminRow = DB::table('saas_admin_audit_log')->orderByDesc('id')->first(); expect($adminRow->action)->toBe('supplier_integration.export_mode_set') ->and($adminRow->admin_user_id)->toBe($adminId); // ── Step 6: Webhook with bad secret → webhook_log (rejected_secret) ────── // Ensure the known-good secret is set so a *wrong* secret triggers rejection SystemSetting::query() ->where('key', 'supplier_webhook_secret') ->update(['value' => 'test-secret-32chars-aaaaaaaaaaaaaa']); SystemSetting::query() ->where('key', 'supplier_ip_allowlist') ->update(['value' => '[]']); RateLimiter::clear('supplier-webhook:127.0.0.1'); $webhookLogCountBefore = DB::table('webhook_log')->count(); $this->postJson('/api/webhook/supplier/wrong-secret-here', [ 'vid' => 999901, 'project' => 'B1_opflow-test.ru', 'phone' => '79991234567', 'time' => time(), ])->assertStatus(404); expect(DB::table('webhook_log')->count())->toBe($webhookLogCountBefore + 1); $webhookLogRow = DB::table('webhook_log') ->where('status', 'rejected_secret') ->where('source', 'supplier') ->latest('created_at') ->first(); expect($webhookLogRow)->not->toBeNull('rejected_secret must be in webhook_log'); expect($webhookLogRow->status)->toBe('rejected_secret'); // ── Step 7: Failure storm → incidents_log ───────────────────────────────── // Insert 201 failed_webhook_jobs rows (above the 200-threshold) $now = now(); $rows = []; for ($i = 0; $i < 201; $i++) { $rows[] = [ 'failed_at' => $now, 'exception' => 'App\\Exceptions\\WebhookException: opflow-storm', 'raw_payload' => '{}', 'retry_count' => 0, ]; } DB::table('failed_webhook_jobs')->insert($rows); $this->artisan('incidents:watch-failures')->assertSuccessful(); $incident = DB::table('incidents_log') ->where('summary', 'like', '%201%') ->latest('id') ->first(); expect($incident)->not->toBeNull('incidents:watch-failures must create an incident row'); expect($incident->severity)->toBe('high'); // ── Final: confirm all four tables received rows ────────────────────────── expect( DB::table('tenant_operations_log') ->where('tenant_id', $tenant->id) ->count() )->toBeGreaterThanOrEqual(4, 'tenant_operations_log must have ≥4 rows for this tenant'); expect(DB::table('saas_admin_audit_log')->count())->toBeGreaterThanOrEqual(1); expect(DB::table('webhook_log')->where('status', 'rejected_secret')->count())->toBeGreaterThanOrEqual(1); expect(DB::table('incidents_log')->count())->toBeGreaterThanOrEqual(1); });