fix(core,admin_basic,board,ecommerce,page,gdpr,verification): 설정 미러·자동 정리·탈퇴 원자성·일괄 선택 범위 정비
공개이슈 ··· 및 그 전수 해소분. - 설정 저장·복원·초기화가 상주 프로세스 미러를 갱신하도록 단일 소유자화 - 만료 데이터 자동 정리 예약 12종 신설, 파기는 기본키 배치로 분할 - 회원 탈퇴를 전 경로 원자화하고 관리자 상태변경 경로를 정식 탈퇴로 통일 - 일괄 처리 목록의 선택을 화면에 보이는 행으로 한정(selectionScope) - 비밀번호 정책 문구를 도메인 중립 라벨로 정정 - 정적 검사 2종·검증 스킬 설계 정합성 축 신설
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
### Fixed
|
||||
|
||||
- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다.
|
||||
- 회원 탈퇴·삭제 시 동의 이력 정리가 중간에 실패하면 일부 항목만 처리된 채 남던 문제를 수정했습니다 — 이제 전부 처리되거나 전부 취소됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.)
|
||||
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
|
||||
@@ -295,9 +295,12 @@ class GdprConsentService
|
||||
{
|
||||
$activeConsents = $this->statusRepository->getActiveByUserId($userId);
|
||||
|
||||
foreach ($activeConsents as $consent) {
|
||||
$this->updateConsent($userId, null, $consent->consent_key, false, 'withdraw');
|
||||
}
|
||||
// 루프 중간에 실패하면 일부 항목만 철회된 채 남는다 — 전부 철회되거나 전부 취소되게 한다.
|
||||
DB::transaction(function () use ($userId, $activeConsents) {
|
||||
foreach ($activeConsents as $consent) {
|
||||
$this->updateConsent($userId, null, $consent->consent_key, false, 'withdraw');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,8 +315,11 @@ class GdprConsentService
|
||||
*/
|
||||
public function purgeOnUserDelete(int $userId): void
|
||||
{
|
||||
$this->statusRepository->deleteByUserId($userId);
|
||||
$this->historyRepository->anonymizeForUser($userId);
|
||||
// 삭제만 되고 익명화가 실패하면 감사 이력에 회원 식별자가 그대로 남는다.
|
||||
DB::transaction(function () use ($userId) {
|
||||
$this->statusRepository->deleteByUserId($userId);
|
||||
$this->historyRepository->anonymizeForUser($userId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\Gdpr\Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Mockery;
|
||||
use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentHistoryRepositoryInterface;
|
||||
use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentRepositoryInterface;
|
||||
use Plugins\Sirsoft\Gdpr\Services\GdprConsentService;
|
||||
use Plugins\Sirsoft\Gdpr\Tests\PluginTestCase;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 동의 이력 정리 원자성 테스트 (공개이슈 #112)
|
||||
*
|
||||
* 탈퇴 시 항목별 철회는 루프이고, 삭제 시 status 삭제와 history 익명화는 2단계다.
|
||||
* 중간에 실패하면 일부 항목만 철회되거나, 삭제만 되고 감사 이력에 식별자가 남는다.
|
||||
*
|
||||
* @scenario trigger=withdraw, trigger=delete, failure_injection=mid_loop, failure_injection=second_step
|
||||
*
|
||||
* @effects partial_revoke_rolled_back, delete_and_anonymize_are_atomic
|
||||
*/
|
||||
class GdprConsentAtomicityTest extends PluginTestCase
|
||||
{
|
||||
/**
|
||||
* 탈퇴 철회 루프가 중간에 실패하면 앞서 철회된 항목도 되돌아간다.
|
||||
*/
|
||||
public function test_revoke_all_on_withdraw_rolls_back_partial_loop(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$service = app(GdprConsentService::class);
|
||||
|
||||
// 동의 2건을 만든 뒤 철회 루프를 중간에서 실패시킨다
|
||||
$service->updateConsents($user->id, null, ['marketing' => true, 'analytics' => true], 'signup');
|
||||
|
||||
$activeBefore = app(GdprUserConsentRepositoryInterface::class)
|
||||
->getActiveByUserId($user->id)
|
||||
->count();
|
||||
|
||||
$this->assertGreaterThanOrEqual(2, $activeBefore, '철회 대상 동의가 2건 이상이어야 합니다');
|
||||
|
||||
// history 기록을 실패시켜 루프 중간에서 예외를 유발한다
|
||||
$failing = Mockery::mock(app(GdprUserConsentHistoryRepositoryInterface::class));
|
||||
$failing->shouldReceive('record')->andThrow(new RuntimeException('이력 기록 실패'));
|
||||
$this->app->instance(GdprUserConsentHistoryRepositoryInterface::class, $failing);
|
||||
|
||||
try {
|
||||
app(GdprConsentService::class)->revokeAllOnWithdraw($user->id);
|
||||
$this->fail('철회 루프 실패가 전파되어야 합니다.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertStringContainsString('이력 기록 실패', $e->getMessage());
|
||||
}
|
||||
|
||||
// 실패 시: 앞 항목만 철회되고 뒤 항목은 그대로인 반쪽 상태
|
||||
$this->app->forgetInstance(GdprUserConsentHistoryRepositoryInterface::class);
|
||||
$activeAfter = app(GdprUserConsentRepositoryInterface::class)
|
||||
->getActiveByUserId($user->id)
|
||||
->count();
|
||||
|
||||
$this->assertSame($activeBefore, $activeAfter, '철회가 일부만 반영된 채 남았습니다');
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 시 history 익명화가 실패하면 status 삭제도 되돌아간다.
|
||||
*/
|
||||
public function test_purge_on_user_delete_rolls_back_when_anonymize_fails(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$service = app(GdprConsentService::class);
|
||||
|
||||
$service->updateConsents($user->id, null, ['marketing' => true], 'signup');
|
||||
|
||||
$before = app(GdprUserConsentRepositoryInterface::class)->getActiveByUserId($user->id)->count();
|
||||
$this->assertGreaterThan(0, $before);
|
||||
|
||||
$failing = Mockery::mock(app(GdprUserConsentHistoryRepositoryInterface::class));
|
||||
$failing->shouldReceive('anonymizeForUser')->andThrow(new RuntimeException('익명화 실패'));
|
||||
$this->app->instance(GdprUserConsentHistoryRepositoryInterface::class, $failing);
|
||||
|
||||
try {
|
||||
app(GdprConsentService::class)->purgeOnUserDelete($user->id);
|
||||
$this->fail('익명화 실패가 전파되어야 합니다.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertStringContainsString('익명화 실패', $e->getMessage());
|
||||
}
|
||||
|
||||
// 실패 시: 동의 status 는 지워졌는데 감사 이력에는 회원 식별자가 남는 상태
|
||||
$this->app->forgetInstance(GdprUserConsentHistoryRepositoryInterface::class);
|
||||
$this->assertSame(
|
||||
$before,
|
||||
app(GdprUserConsentRepositoryInterface::class)->getActiveByUserId($user->id)->count()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
### Fixed
|
||||
|
||||
- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다.
|
||||
- 회원 탈퇴·삭제 시 본인확인 기록 파기와 이력 익명화 중 한쪽만 처리된 채 남을 수 있던 문제를 수정했습니다 — 이제 두 처리가 함께 완료됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.)
|
||||
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
|
||||
+7
-2
@@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationKginicis\Listeners;
|
||||
|
||||
use App\Contracts\Extension\HookListenerInterface;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityLogQueryRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityRecordRepositoryInterface;
|
||||
|
||||
@@ -69,7 +70,11 @@ class CleanInicisRecordOnUserDelete implements HookListenerInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
// record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면
|
||||
// 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다.
|
||||
DB::transaction(function () use ($userId) {
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationKginicis\Listeners;
|
||||
|
||||
use App\Contracts\Extension\HookListenerInterface;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityLogQueryRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityRecordRepositoryInterface;
|
||||
|
||||
@@ -60,7 +61,11 @@ class CleanInicisRecordOnUserWithdraw implements HookListenerInterface
|
||||
|
||||
$userId = (int) $user->id;
|
||||
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
// record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면
|
||||
// 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다.
|
||||
DB::transaction(function () use ($userId) {
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\VerificationKginicis\Tests\Feature\Listeners;
|
||||
|
||||
use App\Enums\IdentityVerificationStatus;
|
||||
use App\Extension\HookManager;
|
||||
use App\Models\IdentityVerificationLog;
|
||||
use App\Models\User;
|
||||
use App\Services\IdentityLogService;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Mockery;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityLogQueryRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityRecordRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationKginicis\Tests\PluginTestCase;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 본인확인 정리 원자성 + 연쇄 삭제 테스트 (공개이슈 #112 / #110)
|
||||
*
|
||||
* record 파기와 로그 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 파기된 것으로 보이는데
|
||||
* 이력에 식별자가 남거나, 그 반대가 된다.
|
||||
*
|
||||
* 함께, 보관주기 파기(identity:prune-logs)가 challenge 매핑까지 연쇄 삭제하는지 고정한다.
|
||||
*
|
||||
* @scenario trigger=withdraw, trigger=delete, trigger=prune_logs, failure_injection=log_anonymize_fails
|
||||
*
|
||||
* @effects record_purge_rolled_back, half_done_state_prevented, prune_logs_cascades_to_challenge_mappings
|
||||
*/
|
||||
class CleanInicisRecordAtomicityTest extends PluginTestCase
|
||||
{
|
||||
/**
|
||||
* 로그 익명화가 실패하면 record 파기도 되돌아간다.
|
||||
*/
|
||||
public function test_record_purge_rolls_back_when_log_anonymize_fails(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->seedRecordForUser($user);
|
||||
|
||||
$this->assertDatabaseHas('inicis_identity_records', ['user_id' => $user->id]);
|
||||
|
||||
// 두 번째 단계(로그 익명화)만 실패시킨다
|
||||
$failing = Mockery::mock(app(InicisIdentityLogQueryRepositoryInterface::class));
|
||||
$failing->shouldReceive('anonymizeUserId')->andThrow(new RuntimeException('익명화 실패'));
|
||||
$this->app->instance(InicisIdentityLogQueryRepositoryInterface::class, $failing);
|
||||
|
||||
try {
|
||||
HookManager::doAction('core.user.after_withdraw', $user);
|
||||
$this->fail('로그 익명화 실패가 전파되어야 합니다.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertStringContainsString('익명화 실패', $e->getMessage());
|
||||
}
|
||||
|
||||
// 선행 단계(record 파기)가 원상 복구되어야 한다
|
||||
// (실패 시: record 만 파기되고 이력 익명화는 실패한 반쪽 상태)
|
||||
$this->assertDatabaseHas('inicis_identity_records', ['user_id' => $user->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 훅에서도 같은 원자성이 보장된다.
|
||||
*/
|
||||
public function test_delete_hook_rolls_back_when_log_anonymize_fails(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->seedRecordForUser($user);
|
||||
|
||||
$failing = Mockery::mock(app(InicisIdentityLogQueryRepositoryInterface::class));
|
||||
$failing->shouldReceive('anonymizeUserId')->andThrow(new RuntimeException('익명화 실패'));
|
||||
$this->app->instance(InicisIdentityLogQueryRepositoryInterface::class, $failing);
|
||||
|
||||
try {
|
||||
HookManager::doAction('core.user.before_delete', $user);
|
||||
$this->fail('로그 익명화 실패가 전파되어야 합니다.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertStringContainsString('익명화 실패', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->assertDatabaseHas('inicis_identity_records', ['user_id' => $user->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* B6 연쇄: 보관주기 파기가 challenge 매핑까지 함께 지운다.
|
||||
*/
|
||||
public function test_prune_logs_cascades_to_challenge_mappings(): void
|
||||
{
|
||||
$expiredLog = $this->seedChallengeLog(now()->subDays(200));
|
||||
$recentLog = $this->seedChallengeLog(now()->subDays(10));
|
||||
|
||||
$this->assertDatabaseHas('inicis_challenge_mappings', ['challenge_id' => $expiredLog]);
|
||||
$this->assertDatabaseHas('inicis_challenge_mappings', ['challenge_id' => $recentLog]);
|
||||
|
||||
app(IdentityLogService::class)->purge(180);
|
||||
|
||||
$this->assertDatabaseMissing('identity_verification_logs', ['id' => $expiredLog]);
|
||||
// 실패 시: 이력은 파기됐는데 challenge 매핑이 고아로 남은 상태
|
||||
$this->assertDatabaseMissing('inicis_challenge_mappings', ['challenge_id' => $expiredLog]);
|
||||
|
||||
// 보관기간 이내 이력과 그 매핑은 보존된다
|
||||
$this->assertDatabaseHas('identity_verification_logs', ['id' => $recentLog]);
|
||||
$this->assertDatabaseHas('inicis_challenge_mappings', ['challenge_id' => $recentLog]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 본인확인 record 를 시드합니다.
|
||||
*
|
||||
* @param User $user 대상 사용자
|
||||
*/
|
||||
private function seedRecordForUser(User $user): void
|
||||
{
|
||||
app(InicisIdentityRecordRepositoryInterface::class)->upsertForUser((int) $user->id, [
|
||||
'name_encrypted' => Crypt::encryptString('홍길동'),
|
||||
'phone_encrypted' => Crypt::encryptString('01012345678'),
|
||||
'birthday_encrypted' => Crypt::encryptString('19900101'),
|
||||
'di_encrypted' => Crypt::encryptString('DI-VAL-'.$user->id),
|
||||
'di_hash' => hash('sha256', 'DI-VAL-'.$user->id),
|
||||
'gender' => 'M',
|
||||
'is_foreigner' => false,
|
||||
'is_adult' => true,
|
||||
'provider_dev_cd' => 'SKT',
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* challenge 로그 + mTxId 매핑을 시드합니다.
|
||||
*
|
||||
* @param Carbon $createdAt 생성 시각
|
||||
* @return string 생성된 challenge UUID
|
||||
*/
|
||||
private function seedChallengeLog($createdAt): string
|
||||
{
|
||||
$log = IdentityVerificationLog::create([
|
||||
'id' => (string) Str::uuid(),
|
||||
'provider_id' => 'inicis',
|
||||
'purpose' => 'signup',
|
||||
'channel' => 'sms',
|
||||
'target_hash' => hash('sha256', Str::random()),
|
||||
'status' => IdentityVerificationStatus::Verified->value,
|
||||
'expires_at' => $createdAt,
|
||||
]);
|
||||
$log->forceFill(['created_at' => $createdAt])->saveQuietly();
|
||||
|
||||
DB::table('inicis_challenge_mappings')->insert([
|
||||
'mtxid' => Str::random(18),
|
||||
'challenge_id' => $log->id,
|
||||
'created_at' => $createdAt,
|
||||
]);
|
||||
|
||||
return $log->id;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
### Fixed
|
||||
|
||||
- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다.
|
||||
- 회원 탈퇴·삭제 시 본인확인 기록 파기와 이력 익명화 중 한쪽만 처리된 채 남을 수 있던 문제를 수정했습니다 — 이제 두 처리가 함께 완료됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.)
|
||||
|
||||
## [1.0.0] - 2026-08-10
|
||||
|
||||
|
||||
+7
-2
@@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationNhnkcp\Listeners;
|
||||
|
||||
use App\Contracts\Extension\HookListenerInterface;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityLogQueryRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityRecordRepositoryInterface;
|
||||
|
||||
@@ -61,7 +62,11 @@ class CleanKcpRecordOnUserDelete implements HookListenerInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
// record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면
|
||||
// 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다.
|
||||
DB::transaction(function () use ($userId) {
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationNhnkcp\Listeners;
|
||||
|
||||
use App\Contracts\Extension\HookListenerInterface;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityLogQueryRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityRecordRepositoryInterface;
|
||||
|
||||
@@ -59,7 +60,11 @@ class CleanKcpRecordOnUserWithdraw implements HookListenerInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
// record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면
|
||||
// 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다.
|
||||
DB::transaction(function () use ($userId) {
|
||||
$this->recordRepository->deleteByUserId($userId);
|
||||
$this->logQueryRepository->anonymizeUserId($userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\VerificationNhnkcp\Tests\Feature\Listeners;
|
||||
|
||||
use App\Extension\HookManager;
|
||||
use App\Models\User;
|
||||
use Mockery;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityLogQueryRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityRecordRepositoryInterface;
|
||||
use Plugins\Sirsoft\VerificationNhnkcp\Tests\PluginTestCase;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 본인확인 정리 원자성 테스트 (공개이슈 #112)
|
||||
*
|
||||
* record 파기와 로그 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 파기된 것으로 보이는데
|
||||
* 이력에 식별자가 남거나, 그 반대가 된다.
|
||||
*
|
||||
* @scenario trigger=withdraw, trigger=delete, failure_injection=log_anonymize_fails
|
||||
*
|
||||
* @effects record_purge_rolled_back, half_done_state_prevented
|
||||
*/
|
||||
class CleanKcpRecordAtomicityTest extends PluginTestCase
|
||||
{
|
||||
/**
|
||||
* 탈퇴 훅: 로그 익명화가 실패하면 record 파기도 되돌아간다.
|
||||
*/
|
||||
public function test_record_purge_rolls_back_when_log_anonymize_fails_on_withdraw(): void
|
||||
{
|
||||
$user = $this->seedUserWithRecord();
|
||||
|
||||
$this->failLogAnonymize();
|
||||
|
||||
try {
|
||||
HookManager::doAction('core.user.after_withdraw', $user);
|
||||
$this->fail('로그 익명화 실패가 전파되어야 합니다.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertStringContainsString('익명화 실패', $e->getMessage());
|
||||
}
|
||||
|
||||
// 실패 시: record 만 파기되고 이력에는 식별자가 남는 반쪽 상태
|
||||
$this->assertNotNull(
|
||||
app(KcpIdentityRecordRepositoryInterface::class)->findByUserId($user->id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 훅에서도 같은 원자성이 보장된다.
|
||||
*/
|
||||
public function test_record_purge_rolls_back_when_log_anonymize_fails_on_delete(): void
|
||||
{
|
||||
$user = $this->seedUserWithRecord();
|
||||
|
||||
$this->failLogAnonymize();
|
||||
|
||||
try {
|
||||
HookManager::doAction('core.user.before_delete', $user);
|
||||
$this->fail('로그 익명화 실패가 전파되어야 합니다.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertStringContainsString('익명화 실패', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->assertNotNull(
|
||||
app(KcpIdentityRecordRepositoryInterface::class)->findByUserId($user->id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* record 를 가진 사용자를 만듭니다.
|
||||
*
|
||||
* @return User 생성된 사용자
|
||||
*/
|
||||
private function seedUserWithRecord(): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
app(KcpIdentityRecordRepositoryInterface::class)->upsertForUser($user->id, [
|
||||
'di_hash' => hash_hmac('sha256', 'DI-ATOMIC-'.$user->id, (string) config('app.key')),
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertNotNull(app(KcpIdentityRecordRepositoryInterface::class)->findByUserId($user->id));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 번째 단계(로그 익명화)만 실패시킵니다.
|
||||
*/
|
||||
private function failLogAnonymize(): void
|
||||
{
|
||||
$failing = Mockery::mock(app(KcpIdentityLogQueryRepositoryInterface::class));
|
||||
$failing->shouldReceive('anonymizeUserId')->andThrow(new RuntimeException('익명화 실패'));
|
||||
|
||||
$this->app->instance(KcpIdentityLogQueryRepositoryInterface::class, $failing);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user