Files
portal/app/tests/Feature/Admin/AdminPdSubjectRequestsControllerTest.php
T

329 lines
12 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use App\Services\Pd\PdErasureService;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Создать stub saas_admin_users и вернуть его id. */
function pdStubAdminUser(string $email = 'pd-test-stub@system.local'): int
{
$existing = DB::table('saas_admin_users')->where('email', $email)->value('id');
if ($existing !== null) {
return (int) $existing;
}
return (int) DB::table('saas_admin_users')->insertGetId([
'email' => $email,
'full_name' => 'PD Test Stub',
'password_hash' => '$2y$04$system-stub-not-loginable',
'role' => 'super_admin',
'is_active' => false,
'sso_provider' => 'local',
'is_break_glass' => false,
]);
}
/** Создать тенант и вернуть его. */
function pdCreateTenant(): Tenant
{
return Tenant::factory()->create([
'subdomain' => 'pd-test-'.uniqid(),
'organization_name' => 'PD Test Org',
'contact_email' => 'pd-tenant@test.local',
'status' => 'active',
]);
}
/** Вставить запись pd_subject_requests напрямую и вернуть id. */
function pdInsertRequest(array $attrs = []): int
{
$defaults = [
'received_at' => now(),
'subject_email' => 'subject@example.com',
'subject_phone' => null,
'subject_full_name' => 'Test Subject',
'request_type' => 'deletion',
'description' => 'Test description',
'status' => 'received',
'tenant_id' => null,
'processing_restricted' => false,
// deadline_at заполняется триггером, но NOT NULL — вставим вручную
'deadline_at' => now()->addDays(30),
];
return (int) DB::connection('pgsql_supplier')
->table('pd_subject_requests')
->insertGetId(array_merge($defaults, $attrs));
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
it('index returns paginated list of pd_subject_requests', function (): void {
$this->actingAs(User::factory()->create());
// Вставим 2 записи
pdInsertRequest(['request_type' => 'deletion']);
pdInsertRequest(['request_type' => 'access', 'status' => 'completed']);
$response = $this->getJson('/api/admin/pd-subject-requests');
$response->assertOk();
expect($response->json('total'))->toBeGreaterThanOrEqual(2);
expect($response->json('data'))->toBeArray();
$first = $response->json('data.0');
expect($first)->toHaveKeys(['id', 'received_at', 'request_type', 'status', 'deadline_at']);
});
it('index filters by status', function (): void {
$this->actingAs(User::factory()->create());
pdInsertRequest(['status' => 'received']);
pdInsertRequest(['status' => 'completed', 'request_type' => 'access']);
$response = $this->getJson('/api/admin/pd-subject-requests?status=received');
$response->assertOk();
foreach ($response->json('data') as $row) {
expect($row['status'])->toBe('received');
}
});
it('store creates pd_subject_request with deadline_at ~+30 days from received_at', function (): void {
$this->actingAs(User::factory()->create());
$response = $this->postJson('/api/admin/pd-subject-requests', [
'subject_email' => 'newsubject@example.com',
'request_type' => 'deletion',
'description' => 'Please delete my data.',
]);
$response->assertCreated();
$data = $response->json('data');
expect($data['subject_email'])->toBe('newsubject@example.com');
expect($data['request_type'])->toBe('deletion');
expect($data['status'])->toBe('received');
// deadline_at должен быть ~30 дней вперёд (с погрешностью ±2 дня на тест-лаги)
$deadline = CarbonImmutable::parse($data['deadline_at']);
$received = CarbonImmutable::parse($data['received_at']);
// diffInDays: абсолютное значение (порядок параметров не важен с abs)
$diff = abs($deadline->diffInDays($received));
expect($diff)->toBeGreaterThanOrEqual(29)->toBeLessThanOrEqual(31);
});
it('store validates: at least email or phone required', function (): void {
$this->actingAs(User::factory()->create());
$this->postJson('/api/admin/pd-subject-requests', [
'request_type' => 'deletion',
])->assertStatus(422);
});
it('store validates: request_type must be valid', function (): void {
$this->actingAs(User::factory()->create());
$this->postJson('/api/admin/pd-subject-requests', [
'subject_email' => 'x@y.com',
'request_type' => 'invalid_type',
])->assertStatus(422);
});
it('executeErasure anonymises user email first_name phone and writes pd_processing_log', function (): void {
$this->actingAs(User::factory()->create());
// Используем pgsql_supplier для всех вставок, чтобы FK-проверки работали
// в рамках одного соединения (DatabaseTransactions оборачивает default pgsql,
// но pgsql_supplier видит только committed данные default-соединения).
$stubEmail = 'pd-user-stub-'.uniqid().'@system.local';
$adminId = (int) DB::connection('pgsql_supplier')->table('saas_admin_users')->insertGetId([
'email' => $stubEmail,
'full_name' => 'User Test Stub',
'password_hash' => '$2y$04$system-stub-not-loginable',
'role' => 'super_admin',
'is_active' => false,
'sso_provider' => 'local',
'is_break_glass' => false,
]);
// Создаём тенант через pgsql_supplier (тот же физ. сервер/БД)
$tenantId = (int) DB::connection('pgsql_supplier')->table('tenants')->insertGetId([
'subdomain' => 'pd-user-test-'.uniqid(),
'organization_name' => 'PD User Test',
'contact_email' => 'pd-u@test.local',
'status' => 'active',
'balance_rub' => '0.00',
'balance_leads' => 0,
'is_trial' => false,
'chargeback_unrecovered_rub' => '0.00',
'created_at' => now(),
]);
// Создаём user с email/phone субъекта
$victimEmail = 'victim-'.uniqid().'@example.com';
$victimPhone = '+79991234567';
$userId = (int) DB::connection('pgsql_supplier')->table('users')->insertGetId([
'tenant_id' => $tenantId,
'email' => $victimEmail,
'password_hash' => '$2y$04$test',
'first_name' => 'Иван',
'last_name' => 'Иванов',
'phone' => $victimPhone,
'is_active' => true,
'created_at' => now(),
]);
$requestId = pdInsertRequest([
'subject_email' => $victimEmail,
'subject_phone' => $victimPhone,
'tenant_id' => $tenantId,
'request_type' => 'deletion',
'status' => 'received',
]);
$response = $this->postJson("/api/admin/pd-subject-requests/{$requestId}/erase", [
'admin_user_id' => $adminId,
]);
$response->assertOk();
expect($response->json('counts.users'))->toBe(1);
// Проверяем анонимизацию user
$user = DB::connection('pgsql_supplier')->table('users')->where('id', $userId)->first();
expect($user->email)->toContain('erased-');
expect($user->first_name)->toBe('Удалено');
expect($user->phone)->toContain('+7000');
// pd_processing_log должен содержать запись
$log = DB::connection('pgsql_supplier')
->table('pd_processing_log')
->where('subject_id', $userId)
->where('subject_type', 'user')
->where('action', 'deleted')
->where('actor_admin_user_id', $adminId)
->first();
expect($log)->not->toBeNull();
expect($log->purpose)->toBe('152-FZ erasure');
});
it('executeErasure anonymises supplier_lead phone and raw_payload', function (): void {
$this->actingAs(User::factory()->create());
// Создаём stub admin через pgsql_supplier, чтобы FK pd_processing_log работал
// независимо от DatabaseTransactions-транзакции default-соединения.
$stubEmail = 'pd-lead-stub-'.uniqid().'@system.local';
$adminId = (int) DB::connection('pgsql_supplier')->table('saas_admin_users')->insertGetId([
'email' => $stubEmail,
'full_name' => 'Lead Test Stub',
'password_hash' => '$2y$04$system-stub-not-loginable',
'role' => 'super_admin',
'is_active' => false,
'sso_provider' => 'local',
'is_break_glass' => false,
]);
$victimPhone = '+79887654321';
$leadId = (int) DB::connection('pgsql_supplier')->table('supplier_leads')->insertGetId([
'platform' => 'B1',
'raw_payload' => json_encode(['phone' => $victimPhone, 'name' => 'Жертва']),
'phone' => $victimPhone,
'received_at' => now(),
'source' => 'webhook',
]);
$requestId = pdInsertRequest([
'subject_phone' => $victimPhone,
'request_type' => 'deletion',
'status' => 'received',
]);
$response = $this->postJson("/api/admin/pd-subject-requests/{$requestId}/erase", [
'admin_user_id' => $adminId,
]);
$response->assertOk();
expect($response->json('counts.leads'))->toBeGreaterThanOrEqual(1);
$lead = DB::connection('pgsql_supplier')->table('supplier_leads')->where('id', $leadId)->first();
expect($lead->phone)->toBe('+7000XXXXXXX');
$payload = json_decode($lead->raw_payload, true);
expect($payload['erased'])->toBe(true);
});
it('executeErasure marks pd_subject_request as completed', function (): void {
$this->actingAs(User::factory()->create());
$adminId = pdStubAdminUser();
$requestId = pdInsertRequest([
'subject_email' => 'mark-completed-'.uniqid().'@example.com',
'request_type' => 'deletion',
'status' => 'received',
]);
$this->postJson("/api/admin/pd-subject-requests/{$requestId}/erase", [
'admin_user_id' => $adminId,
])->assertOk();
$row = DB::connection('pgsql_supplier')
->table('pd_subject_requests')
->where('id', $requestId)
->first();
expect($row->status)->toBe('completed');
expect($row->completed_at)->not->toBeNull();
expect($row->response_text)->toContain('users=');
});
it('executeErasure rejects non-deletion request_type with 422', function (): void {
$this->actingAs(User::factory()->create());
$requestId = pdInsertRequest([
'subject_email' => 'access-request@example.com',
'request_type' => 'access',
'status' => 'received',
]);
$this->postJson("/api/admin/pd-subject-requests/{$requestId}/erase")
->assertStatus(422);
});
it('executeErasure rejects already completed request with 422', function (): void {
$this->actingAs(User::factory()->create());
$requestId = pdInsertRequest([
'subject_email' => 'already-done-'.uniqid().'@example.com',
'request_type' => 'deletion',
'status' => 'completed',
]);
$this->postJson("/api/admin/pd-subject-requests/{$requestId}/erase")
->assertStatus(422);
});
it('saas-admin middleware allows request in testing env', function (): void {
// EnsureSaasAdmin в testing-окружении пропускает всех без проверки.
$response = $this->getJson('/api/admin/pd-subject-requests');
$response->assertOk();
});
it('PdErasureService throws InvalidArgumentException when both email and phone are null', function (): void {
$service = app(PdErasureService::class);
expect(fn () => $service->eraseSubject(null, null, null, 1, null))
->toThrow(InvalidArgumentException::class);
});