KISA 제보 취약점(KVE-2026-1914/1919/2019/2029/2041/2042/2043/2044)과 그 수정 과정에서 드러난 자격증명 전송로 결함을 함께 해소한다. globalHeaders 는 데이터소스와 apiCall 핸들러에만 적용되는데, 코어 ApiClient 를 직접 부르는 경로들이 그 사실을 모른 채 게이트된 엔드포인트를 호출하고 있었다. 서버는 정당한 사용자를 거부하고 화면은 이미 버튼을 내준 뒤라, 예외도 로그도 없이 그 자리만 비는 형태로만 드러났다. 전송로 10축을 전수 열거해 6건을 고치고, 같은 실수가 반복되지 않도록 규정과 coverage 에 등재했다. 아웃바운드 프록시가 사이트 자기 자신으로 가는 내부 요청까지 가로채 저장이 수십 초씩 걸리던 문제도 함께 고쳤다. 실패가 폴백으로 삼켜져 화면에는 지연으로만 나타났다.
663 lines
26 KiB
PHP
663 lines
26 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Feature\User;
|
|
|
|
// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요)
|
|
require_once __DIR__.'/../../ModuleTestCase.php';
|
|
|
|
use App\Models\Permission;
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Modules\Sirsoft\Board\Services\PostService;
|
|
use Modules\Sirsoft\Board\Tests\BoardTestCase;
|
|
|
|
/**
|
|
* 비밀글 비밀번호 검증 API 테스트
|
|
*
|
|
* 비밀글(is_secret=true)의 비밀번호 검증 엔드포인트를 테스트합니다.
|
|
* - verify-password: 비밀글 조회용 (posts.read 권한 필요)
|
|
* - verify-password-for-modify: 비밀글 수정용 (posts.write 권한 필요)
|
|
*
|
|
* 테스트 시나리오:
|
|
* - 비회원 비밀글 비밀번호 검증 (읽기/수정)
|
|
* - 회원 비밀글 비밀번호 검증 (작성자/타인)
|
|
* - 관리자 권한 우회
|
|
* - 잘못된 비밀번호 처리
|
|
*/
|
|
class PostSecretVerifyPasswordTest extends BoardTestCase
|
|
{
|
|
private User $boardAdminUser;
|
|
|
|
private User $secretPostReadUser;
|
|
|
|
private User $regularUser;
|
|
|
|
/**
|
|
* 테스트 게시판 slug
|
|
*/
|
|
protected function getTestBoardSlug(): string
|
|
{
|
|
return 'post-secret-verify';
|
|
}
|
|
|
|
/**
|
|
* 기본 게시판 속성 (비밀글 기능 활성화)
|
|
*/
|
|
protected function getDefaultBoardAttributes(string $slug): array
|
|
{
|
|
return [
|
|
'slug' => $slug,
|
|
'name' => ['ko' => '비밀글 검증 테스트 게시판', 'en' => 'Secret Post Verify Test Board'],
|
|
'is_active' => true,
|
|
'use_file_upload' => true,
|
|
'secret_mode' => 'enabled',
|
|
'blocked_keywords' => [],
|
|
];
|
|
}
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// 테스트용 사용자 생성
|
|
$this->createTestUsers();
|
|
}
|
|
|
|
/**
|
|
* 테스트용 사용자 생성
|
|
*/
|
|
private function createTestUsers(): void
|
|
{
|
|
$slug = $this->board->slug;
|
|
|
|
// 공통 사용자 권한: posts.read, posts.write (라우트 접근에 필요)
|
|
$postsReadPermission = Permission::firstOrCreate(
|
|
['identifier' => "sirsoft-board.{$slug}.posts.read"],
|
|
[
|
|
'name' => ['ko' => '게시글 조회', 'en' => 'Read Posts'],
|
|
'slug' => "sirsoft-board.{$slug}.posts.read",
|
|
'type' => 'user',
|
|
]
|
|
);
|
|
$postsWritePermission = Permission::firstOrCreate(
|
|
['identifier' => "sirsoft-board.{$slug}.posts.write"],
|
|
[
|
|
'name' => ['ko' => '게시글 작성', 'en' => 'Write Posts'],
|
|
'slug' => "sirsoft-board.{$slug}.posts.write",
|
|
'type' => 'user',
|
|
]
|
|
);
|
|
|
|
$userRole = Role::firstOrCreate(
|
|
['identifier' => "{$slug}-user"],
|
|
['name' => ['ko' => '일반 사용자', 'en' => 'Regular User']]
|
|
);
|
|
$userRole->permissions()->syncWithoutDetaching([
|
|
$postsReadPermission->id,
|
|
$postsWritePermission->id,
|
|
]);
|
|
|
|
// 1. 게시판 관리자 (admin.posts.read + admin.manage + user 라우트 접근용 posts.read)
|
|
$this->boardAdminUser = User::factory()->create();
|
|
$adminPostsReadPermission = Permission::firstOrCreate(
|
|
['identifier' => "sirsoft-board.{$slug}.admin.posts.read"],
|
|
[
|
|
'name' => ['ko' => '관리자 게시글 조회', 'en' => 'Admin Posts Read'],
|
|
'slug' => "sirsoft-board.{$slug}.admin.posts.read",
|
|
'type' => 'admin',
|
|
]
|
|
);
|
|
$managePermission = Permission::firstOrCreate(
|
|
['identifier' => "sirsoft-board.{$slug}.admin.manage"],
|
|
[
|
|
'name' => ['ko' => '관리', 'en' => 'Manage'],
|
|
'slug' => "sirsoft-board.{$slug}.admin.manage",
|
|
'type' => 'admin',
|
|
]
|
|
);
|
|
$boardAdminRole = Role::firstOrCreate(
|
|
['identifier' => "{$slug}-board-admin"],
|
|
['name' => ['ko' => '게시판 관리자', 'en' => 'Board Admin']]
|
|
);
|
|
$boardAdminRole->permissions()->syncWithoutDetaching([
|
|
$adminPostsReadPermission->id,
|
|
$managePermission->id,
|
|
]);
|
|
$this->boardAdminUser->roles()->attach($boardAdminRole->id);
|
|
$this->boardAdminUser->roles()->attach($userRole->id);
|
|
|
|
// 2. 비밀글 읽기 권한 사용자 (posts.read-secret + posts.read)
|
|
$this->secretPostReadUser = User::factory()->create();
|
|
$readSecretPermission = Permission::firstOrCreate(
|
|
['identifier' => "sirsoft-board.{$slug}.posts.read-secret"],
|
|
[
|
|
'name' => ['ko' => '비밀글 읽기', 'en' => 'Read Secret'],
|
|
'slug' => "sirsoft-board.{$slug}.posts.read-secret",
|
|
'type' => 'user',
|
|
]
|
|
);
|
|
$readSecretRole = Role::firstOrCreate(
|
|
['identifier' => "{$slug}-secret-reader"],
|
|
['name' => ['ko' => '비밀글 열람자', 'en' => 'Secret Reader']]
|
|
);
|
|
$readSecretRole->permissions()->syncWithoutDetaching([$readSecretPermission->id]);
|
|
$this->secretPostReadUser->roles()->attach($readSecretRole->id);
|
|
$this->secretPostReadUser->roles()->attach($userRole->id);
|
|
|
|
// 3. 일반 사용자
|
|
$this->regularUser = User::factory()->create();
|
|
$this->regularUser->roles()->attach($userRole->id);
|
|
}
|
|
|
|
/**
|
|
* 사용자에게 posts.read 권한을 부여합니다.
|
|
*/
|
|
private function grantPostsReadPermission(User $user): void
|
|
{
|
|
$slug = $this->board->slug;
|
|
$userRole = Role::where('identifier', "{$slug}-user")->first();
|
|
|
|
if ($userRole) {
|
|
$user->roles()->syncWithoutDetaching([$userRole->id]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 비회원 비밀글을 생성합니다.
|
|
*/
|
|
private function createGuestSecretPost(string $password = 'test1234'): int
|
|
{
|
|
return DB::table('board_posts')->insertGetId([
|
|
'board_id' => $this->board->id,
|
|
'title' => '비회원 비밀글',
|
|
'content' => '비회원이 작성한 비밀글 내용입니다.',
|
|
'content_mode' => 'text',
|
|
'author_name' => '비회원작성자',
|
|
'password' => Hash::make($password),
|
|
'user_id' => null,
|
|
'is_secret' => true,
|
|
'is_notice' => false,
|
|
'view_count' => 0,
|
|
'status' => 'published',
|
|
'ip_address' => '127.0.0.1',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 회원 비밀글을 생성합니다.
|
|
*/
|
|
private function createMemberSecretPost(User $user, ?string $password = null): int
|
|
{
|
|
return DB::table('board_posts')->insertGetId([
|
|
'board_id' => $this->board->id,
|
|
'title' => '회원 비밀글',
|
|
'content' => '회원이 작성한 비밀글 내용입니다.',
|
|
'content_mode' => 'text',
|
|
'user_id' => $user->id,
|
|
'password' => $password ? Hash::make($password) : null,
|
|
'is_secret' => true,
|
|
'is_notice' => false,
|
|
'view_count' => 0,
|
|
'status' => 'published',
|
|
'ip_address' => '127.0.0.1',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
// ==========================================
|
|
// 비밀글 조회용 비밀번호 검증 (verify-password)
|
|
// ==========================================
|
|
|
|
/**
|
|
* 비회원 비밀글: 올바른 비밀번호로 조회 검증 성공
|
|
*/
|
|
public function test_guest_secret_post_verify_password_success(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 올바른 비밀번호로 검증
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
// Then: 200 성공, content와 attachments 포함
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true)
|
|
->assertJsonStructure([
|
|
'data' => [
|
|
'id',
|
|
'title',
|
|
'content',
|
|
'attachments',
|
|
],
|
|
]);
|
|
|
|
// content가 null이 아님
|
|
$this->assertNotNull($response->json('data.content'));
|
|
}
|
|
|
|
/**
|
|
* 비회원 비밀글: 잘못된 비밀번호로 조회 검증 실패
|
|
*/
|
|
public function test_guest_secret_post_verify_password_wrong_password(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 잘못된 비밀번호로 검증
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
['password' => 'wrongpassword']
|
|
);
|
|
|
|
// Then: 403 실패 (잘못된 비밀번호)
|
|
$response->assertStatus(403)
|
|
->assertJsonPath('success', false);
|
|
}
|
|
|
|
/**
|
|
* 비밀글 조회 검증: 비밀번호 미입력 시 422 에러
|
|
*/
|
|
public function test_verify_password_requires_password_field(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 비밀번호 없이 요청
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
[]
|
|
);
|
|
|
|
// Then: 422 검증 에러
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['password']);
|
|
}
|
|
|
|
// ==========================================
|
|
// 비밀글 수정용 비밀번호 검증 (verify-password-for-modify)
|
|
// ==========================================
|
|
|
|
/**
|
|
* 비회원 비밀글: 올바른 비밀번호로 수정 검증 성공
|
|
*/
|
|
public function test_guest_secret_post_verify_password_for_modify_success(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 올바른 비밀번호로 수정 검증
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password-for-modify",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
// Then: 200 성공, verification_token 포함
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true)
|
|
->assertJsonStructure([
|
|
'data' => [
|
|
'verified',
|
|
'post_id',
|
|
'verification_token',
|
|
'expires_at',
|
|
],
|
|
])
|
|
->assertJsonPath('data.verified', true)
|
|
->assertJsonPath('data.post_id', $postId);
|
|
|
|
// verification_token이 비어있지 않음
|
|
$this->assertNotEmpty($response->json('data.verification_token'));
|
|
}
|
|
|
|
/**
|
|
* 비회원 비밀글: 잘못된 비밀번호로 수정 검증 실패
|
|
*/
|
|
public function test_guest_secret_post_verify_password_for_modify_wrong_password(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 잘못된 비밀번호로 수정 검증
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password-for-modify",
|
|
['password' => 'wrongpassword']
|
|
);
|
|
|
|
// Then: 403 실패 (잘못된 비밀번호)
|
|
$response->assertStatus(403)
|
|
->assertJsonPath('success', false);
|
|
}
|
|
|
|
// ==========================================
|
|
// 회원 비밀글 검증 테스트
|
|
// ==========================================
|
|
|
|
/**
|
|
* 회원 비밀글: 작성자 본인은 비밀번호 없이 조회 가능 (API show 호출)
|
|
*/
|
|
public function test_member_author_can_view_own_secret_post_without_password(): void
|
|
{
|
|
// Given: 회원 비밀글 생성
|
|
$postId = $this->createMemberSecretPost($this->regularUser);
|
|
|
|
// When: 작성자 본인이 조회
|
|
$response = $this->actingAs($this->regularUser)
|
|
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
|
|
|
|
// Then: 200 성공, content 포함
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true);
|
|
|
|
// content가 null이 아님 (작성자 본인은 열람 가능)
|
|
$this->assertNotNull($response->json('data.content'));
|
|
}
|
|
|
|
/**
|
|
* 회원 비밀글: 타인은 content가 null로 반환
|
|
*/
|
|
public function test_member_secret_post_content_hidden_for_others(): void
|
|
{
|
|
// Given: regularUser가 작성한 비밀글
|
|
$postId = $this->createMemberSecretPost($this->regularUser);
|
|
|
|
// When: 다른 사용자(secretPostReadUser가 아닌 일반 사용자)가 조회
|
|
$anotherUser = User::factory()->create();
|
|
$this->grantPostsReadPermission($anotherUser);
|
|
$response = $this->actingAs($anotherUser)
|
|
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
|
|
|
|
// Then: 200 성공하지만 content는 null
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true)
|
|
->assertJsonPath('data.content', null);
|
|
}
|
|
|
|
/**
|
|
* 회원 비밀글: 비밀번호 검증 대상 아님 (400 에러)
|
|
*
|
|
* 회원 비밀글은 로그인 후 본인/권한자만 열람 가능하므로
|
|
* 비밀번호 검증 엔드포인트 사용 불가
|
|
*/
|
|
public function test_member_secret_post_verify_password_not_allowed(): void
|
|
{
|
|
// Given: 비밀번호가 설정된 회원 비밀글
|
|
$postId = $this->createMemberSecretPost($this->regularUser, 'memberpass');
|
|
|
|
// When: 다른 사용자가 비밀번호로 검증 시도
|
|
$anotherUser = User::factory()->create();
|
|
$this->grantPostsReadPermission($anotherUser);
|
|
$response = $this->actingAs($anotherUser, 'sanctum')
|
|
->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
['password' => 'memberpass']
|
|
);
|
|
|
|
// Then: 400 에러 (회원 게시글은 비밀번호 검증 대상 아님)
|
|
$response->assertStatus(400)
|
|
->assertJsonPath('success', false);
|
|
}
|
|
|
|
// ==========================================
|
|
// 권한별 우회 테스트
|
|
// ==========================================
|
|
|
|
/**
|
|
* 관리자(admin.manage 권한)는 관리자 라우트에서 비밀번호 없이 비밀글 조회 가능
|
|
*/
|
|
public function test_admin_can_view_secret_post_without_password(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 관리자가 관리자 라우트로 조회 (admin.manage → content 열람 가능)
|
|
$response = $this->actingAs($this->boardAdminUser, 'sanctum')
|
|
->getJson("/api/modules/sirsoft-board/admin/board/{$this->board->slug}/posts/{$postId}");
|
|
|
|
// Then: 200 성공, content 포함
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true);
|
|
|
|
// 관리자는 비밀번호 없이 content 열람 가능
|
|
$this->assertNotNull($response->json('data.content'));
|
|
}
|
|
|
|
/**
|
|
* 비밀글 읽기 권한(posts.read-secret) 사용자는 비밀번호 없이 조회 가능
|
|
*/
|
|
public function test_read_secret_permission_user_can_view_without_password(): void
|
|
{
|
|
// Given: 비회원 비밀글 생성
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// When: 비밀글 읽기 권한 사용자가 조회
|
|
$response = $this->actingAs($this->secretPostReadUser, 'sanctum')
|
|
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
|
|
|
|
// Then: 200 성공, content 포함
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true);
|
|
|
|
// posts.read-secret 권한이 있으면 비밀번호 없이 열람 가능
|
|
$this->assertNotNull($response->json('data.content'));
|
|
}
|
|
|
|
// ==========================================
|
|
// 존재하지 않는 게시글 테스트
|
|
// ==========================================
|
|
|
|
/**
|
|
* 존재하지 않는 게시글 비밀번호 검증 시 404 에러
|
|
*/
|
|
public function test_verify_password_for_nonexistent_post_returns_404(): void
|
|
{
|
|
// When: 존재하지 않는 게시글에 대해 검증 시도
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/99999/verify-password",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
// Then: 404 에러
|
|
$response->assertStatus(404);
|
|
}
|
|
|
|
/**
|
|
* 존재하지 않는 게시글 수정 검증 시 404 에러
|
|
*/
|
|
public function test_verify_password_for_modify_nonexistent_post_returns_404(): void
|
|
{
|
|
// When: 존재하지 않는 게시글에 대해 수정 검증 시도
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/99999/verify-password-for-modify",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
// Then: 404 에러
|
|
$response->assertStatus(404);
|
|
}
|
|
|
|
// ==========================================
|
|
// 비활성 게시판 테스트
|
|
// ==========================================
|
|
|
|
/**
|
|
* 비활성 게시판의 비밀글 검증 시 404 에러
|
|
*/
|
|
public function test_verify_password_for_inactive_board_returns_404(): void
|
|
{
|
|
// Given: 비밀글 생성 후 게시판 비활성화
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
$this->board->update(['is_active' => false]);
|
|
|
|
// When: 비밀번호 검증 시도
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
// Then: 404 에러
|
|
$response->assertStatus(404);
|
|
}
|
|
|
|
/**
|
|
* 비밀번호 검증 응답은 상세 조회와 같은 스키마다 — 댓글 목록을 포함한다.
|
|
*
|
|
* 화면은 이 응답으로 게시글 데이터소스를 통째로 교체한다. 그래서 응답에 `comments` 가
|
|
* 없으면 목록이 비어 보이는데, 댓글 수(`comment_count`)는 집계 컬럼이라 그대로 남아
|
|
* "댓글 2" 헤더 밑에 아무것도 없는 화면이 된다. 오류도 콘솔 경고도 남지 않고, 댓글을
|
|
* 하나 쓰면 그때 상세를 다시 불러 목록이 나타나므로 원인을 짚기 어렵다.
|
|
*/
|
|
public function test_verify_password_response_includes_comments(): void
|
|
{
|
|
// Given: 댓글 2개가 달린 비회원 비밀글
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
$this->createTestComment($postId, ['content' => '첫 번째 댓글']);
|
|
$this->createTestComment($postId, ['content' => '두 번째 댓글']);
|
|
|
|
// When: 올바른 비밀번호로 검증
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
// Then: 상세 조회와 동일하게 댓글이 실려 온다
|
|
$response->assertStatus(200);
|
|
|
|
$comments = $response->json('data.comments');
|
|
|
|
$this->assertIsArray(
|
|
$comments,
|
|
'비밀번호 검증 응답에 comments 가 없습니다 — 화면이 이 응답으로 데이터소스를 교체하므로 댓글 목록이 통째로 사라집니다.'
|
|
);
|
|
$this->assertCount(2, $comments);
|
|
|
|
// comment_count 는 게시글 행의 집계 컬럼이라 이 픽스처(DB 직접 삽입)에서는 오르지
|
|
// 않는다 — 실제 화면에서 "댓글 2" 헤더 아래가 비어 보였던 것이 이 두 값의 어긋남이다.
|
|
// 여기서 잠그는 계약은 "목록이 응답에 실린다" 쪽이다.
|
|
}
|
|
|
|
/**
|
|
* 수정 검증 토큰은 폼을 불러온 뒤에도 저장에 쓸 수 있어야 한다.
|
|
*
|
|
* 실제 화면 흐름은 「비밀번호 확인 → 폼 조회(form-meta / form-data) → 저장(PUT)」이다.
|
|
* 폼 조회가 토큰을 소비해 버리면 저장 시점에는 남아 있지 않아, 사용자는 비밀번호를
|
|
* 정확히 넣고 본문까지 본 상태에서 「게시글 수정 권한이 없습니다」로 거부된다.
|
|
* 토큰을 새로 받을 방법도 화면에 없어 그 글은 수정 자체가 불가능해진다.
|
|
*/
|
|
public function test_modify_token_survives_form_load_and_authorizes_update(): void
|
|
{
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
|
|
// 1) 비밀번호 확인 → 토큰 발급
|
|
$verify = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password-for-modify",
|
|
['password' => 'test1234']
|
|
);
|
|
$verify->assertStatus(200);
|
|
$token = $verify->json('data.verification_token');
|
|
$this->assertNotEmpty($token);
|
|
|
|
// 2) 화면이 하는 대로 폼 메타·데이터를 그 토큰으로 조회
|
|
$this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/form-meta?post_id={$postId}&verification_token={$token}")
|
|
->assertStatus(200);
|
|
$this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/form-data?post_id={$postId}&verification_token={$token}")
|
|
->assertStatus(200);
|
|
|
|
// 3) 같은 토큰으로 저장
|
|
$update = $this->putJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}",
|
|
[
|
|
'title' => '수정된 제목',
|
|
'content' => '수정된 본문입니다.',
|
|
'verification_token' => $token,
|
|
]
|
|
);
|
|
|
|
$this->assertSame(
|
|
200,
|
|
$update->status(),
|
|
'폼을 불러온 뒤 같은 토큰으로 저장할 수 없습니다 — 폼 조회가 토큰을 소비해 버린 것입니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 수정 검증 토큰을 헤더로 제시해도 폼 조회와 저장이 모두 성립한다.
|
|
*
|
|
* 이 토큰은 종전에 GET 쿼리 파라미터로만 다녔다 — 그러면 웹서버 접근 기록에 자격증명이
|
|
* 그대로 남는다. 헤더로 옮기되 기존 본문/쿼리 경로도 계속 받아야 하므로, 헤더 단독으로도
|
|
* 「폼 메타 → 폼 데이터 → 저장」 전 구간이 성립하는지를 이 테스트가 잠근다.
|
|
*
|
|
* @scenario actor=guest_author, transport=header
|
|
*
|
|
* @effects modify_token_accepted_via_header
|
|
*/
|
|
public function test_modify_token_is_accepted_via_header_without_query_param(): void
|
|
{
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
$base = "/api/modules/sirsoft-board/boards/{$this->board->slug}";
|
|
|
|
$verify = $this->postJson("{$base}/posts/{$postId}/verify-password-for-modify", ['password' => 'test1234']);
|
|
$verify->assertStatus(200);
|
|
$token = (string) $verify->json('data.verification_token');
|
|
$this->assertNotEmpty($token);
|
|
|
|
$header = [PostService::VERIFY_TOKEN_HEADER => $token];
|
|
|
|
$meta = $this->withHeaders($header)->getJson("{$base}/posts/form-meta?post_id={$postId}");
|
|
$meta->assertStatus(200);
|
|
$this->assertFalse(
|
|
(bool) $meta->json('data.requires_password'),
|
|
'헤더로 제시한 검증 토큰이 폼 메타에서 인정되지 않았습니다 — 화면이 비밀번호를 다시 묻습니다.'
|
|
);
|
|
|
|
$data = $this->withHeaders($header)->getJson("{$base}/posts/form-data?post_id={$postId}");
|
|
$data->assertStatus(200);
|
|
$this->assertNotEmpty(
|
|
$data->json('data.content'),
|
|
'헤더로 제시한 검증 토큰이 폼 데이터에서 인정되지 않아 본문이 비어 있습니다.'
|
|
);
|
|
|
|
$update = $this->withHeaders($header)->putJson("{$base}/posts/{$postId}", [
|
|
'title' => '헤더 경로 수정',
|
|
'content' => '헤더로만 토큰을 제시해 저장합니다.',
|
|
]);
|
|
|
|
$this->assertSame(
|
|
200,
|
|
$update->status(),
|
|
'헤더로만 제시한 검증 토큰으로는 저장할 수 없습니다 — 주소에서 자격증명을 뺄 수 없습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 삭제된 비밀글은 비밀번호를 알아도 열리지 않는다 (manager 권한자 제외).
|
|
*
|
|
* 상세 조회(show)는 삭제된 글에 manager 권한을 요구한다. 검증 경로가 그 판정을 하지
|
|
* 않으면 같은 원문이 형제 엔드포인트로 새어나간다 — 화면에서는 목록에 안 보이니
|
|
* 드러나지 않고, 주소를 아는 쪽만 열 수 있다.
|
|
*/
|
|
public function test_verify_password_does_not_open_deleted_post(): void
|
|
{
|
|
$postId = $this->createGuestSecretPost('test1234');
|
|
DB::table('board_posts')->where('id', $postId)->update(['deleted_at' => now()]);
|
|
|
|
$response = $this->postJson(
|
|
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
|
|
['password' => 'test1234']
|
|
);
|
|
|
|
$this->assertContains(
|
|
$response->status(),
|
|
[403, 404],
|
|
'삭제된 비밀글이 비밀번호만으로 열렸습니다 — 상세 조회는 manager 권한을 요구합니다.'
|
|
);
|
|
}
|
|
}
|