Files
2026-04-22 10:43:31 +09:00

311 lines
11 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests\Feature\User;
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
require_once __DIR__.'/../../ModuleTestCase.php';
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
/**
* 비밀글 전수 시나리오 테스트
*
* 검증 목적:
* - secret_mode: disabled / enabled / always 각 모드별 생성/조회 동작
* - posts.read-secret 권한 유무에 따른 비밀글 내용 노출 여부
* - secret_mode=always에서 비회원 접근 동작
* - 비밀번호 기반 비회원 비밀글 접근
*
* @group board
* @group secret
*/
class SecretPostScenarioTest extends BoardTestCase
{
private User $authorUser;
private User $otherUser;
protected function getTestBoardSlug(): string
{
return 'secret-scenario';
}
protected function getDefaultBoardAttributes(string $slug): array
{
return [
'slug' => $slug,
'name' => ['ko' => '비밀글 시나리오 테스트 게시판', 'en' => 'Secret Scenario Board'],
'is_active' => true,
'secret_mode' => 'enabled',
];
}
protected function setUp(): void
{
parent::setUp();
$this->setGuestPermissions(['posts.read', 'posts.write']);
$this->grantUserRolePermissions(['posts.read', 'posts.write']);
$this->authorUser = User::factory()->create(['email' => 'secret-author@test.com']);
$this->otherUser = User::factory()->create(['email' => 'secret-other@test.com']);
$userRole = Role::where('identifier', 'user')->first();
if ($userRole) {
$this->authorUser->roles()->attach($userRole->id);
$this->otherUser->roles()->attach($userRole->id);
}
}
// ==========================================
// secret_mode별 게시글 생성 시나리오
// ==========================================
/**
* secret_mode=disabled이면 비밀글 생성을 거부한다 (403)
*/
public function test_secret_mode_disabled_rejects_is_secret_true(): void
{
$this->updateBoardSettings(['secret_mode' => 'disabled']);
$response = $this->actingAs($this->authorUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비밀글 시도',
'content' => '비밀글로 작성하려는 내용입니다.',
'author_name' => '작성자',
'is_secret' => true,
]);
$response->assertStatus(403);
}
/**
* secret_mode=enabled이면 사용자가 비밀글 여부를 선택할 수 있다
*/
public function test_secret_mode_enabled_allows_user_to_choose(): void
{
$this->updateBoardSettings(['secret_mode' => 'enabled']);
// 비밀글 생성
$secretResponse = $this->actingAs($this->authorUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비밀글 제목',
'content' => '비밀글 본문입니다. 충분한 길이.',
'author_name' => '작성자',
'is_secret' => true,
]);
$secretResponse->assertStatus(201)->assertJsonPath('data.is_secret', true);
// 공개글 생성
$publicResponse = $this->actingAs($this->authorUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '공개글 제목',
'content' => '공개글 본문입니다. 충분한 길이.',
'author_name' => '작성자',
'is_secret' => false,
]);
$publicResponse->assertStatus(201)->assertJsonPath('data.is_secret', false);
}
/**
* secret_mode=always이면 모든 게시글이 비밀글로 강제된다
*/
public function test_secret_mode_always_forces_all_posts_secret(): void
{
$this->updateBoardSettings(['secret_mode' => 'always']);
// is_secret=false로 생성해도 true로 강제
$response = $this->actingAs($this->authorUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '강제 비밀글 제목',
'content' => '강제 비밀글 본문입니다. 충분한 길이.',
'author_name' => '작성자',
'is_secret' => false,
]);
$response->assertStatus(201)->assertJsonPath('data.is_secret', true);
}
/**
* secret_mode=always이면 비회원도 게시글을 작성하면 자동으로 비밀글이 된다
*/
public function test_secret_mode_always_guest_post_becomes_secret(): void
{
$this->updateBoardSettings(['secret_mode' => 'always']);
$response = $this->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비회원 게시글',
'content' => '비회원이 작성하는 게시글 내용입니다.',
'author_name' => '비회원',
'password' => 'pass1234',
'is_secret' => false,
]);
// always 모드에서는 is_secret=false여도 true로 강제됨
$response->assertStatus(201)->assertJsonPath('data.is_secret', true);
}
// ==========================================
// posts.read-secret 권한 분기 시나리오
// ==========================================
/**
* posts.read-secret 권한이 있는 사용자는 비밀글 내용을 볼 수 있다
*/
public function test_user_with_read_secret_permission_can_view_secret_content(): void
{
$postId = $this->createTestPost([
'user_id' => $this->authorUser->id,
'is_secret' => true,
'title' => '비밀 제목',
'content' => '비밀 내용입니다.',
]);
// otherUser에게 read-secret 권한 부여
$perm = Permission::firstOrCreate(
['identifier' => "sirsoft-board.{$this->board->slug}.posts.read-secret"],
['name' => ['ko' => '비밀글 조회', 'en' => 'Read Secret'], 'type' => 'user']
);
$userRole = Role::where('identifier', 'user')->first();
$userRole->permissions()->syncWithoutDetaching([$perm->id]);
$response = $this->actingAs($this->otherUser)
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
$response->assertStatus(200)
->assertJsonPath('data.is_secret', true)
->assertJsonPath('data.content', '비밀 내용입니다.');
}
/**
* posts.read-secret 권한이 없는 타인은 비밀글 내용이 마스킹된다
*/
public function test_user_without_read_secret_sees_masked_content(): void
{
$postId = $this->createTestPost([
'user_id' => $this->authorUser->id,
'is_secret' => true,
'title' => '비밀 제목',
'content' => '비밀 내용입니다.',
]);
$response = $this->actingAs($this->otherUser)
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
$response->assertStatus(200)
->assertJsonPath('data.is_secret', true);
// 내용이 마스킹되거나 빈 값이어야 함
$content = $response->json('data.content');
$this->assertNotEquals('비밀 내용입니다.', $content);
}
/**
* 비밀글 작성자 본인은 항상 내용을 볼 수 있다
*/
public function test_secret_post_author_can_always_view_own_content(): void
{
$postId = $this->createTestPost([
'user_id' => $this->authorUser->id,
'is_secret' => true,
'title' => '내 비밀글',
'content' => '내 비밀 내용입니다.',
]);
$response = $this->actingAs($this->authorUser)
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
$response->assertStatus(200)
->assertJsonPath('data.content', '내 비밀 내용입니다.');
}
/**
* 비회원 비밀글은 올바른 비밀번호로 verify-password API가 content를 반환한다
*/
public function test_guest_secret_post_accessible_with_correct_password(): void
{
$postId = $this->createTestPost([
'is_secret' => true,
'password' => Hash::make('pass1234'),
'author_name' => '비회원',
'title' => '비회원 비밀글',
'content' => '비회원 비밀 내용입니다.',
]);
// 비밀번호 검증 - 성공 시 content 포함하여 반환
$response = $this->postJson(
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
['password' => 'pass1234']
);
$response->assertStatus(200)
->assertJsonPath('success', true)
->assertJsonPath('data.content', '비회원 비밀 내용입니다.');
}
/**
* 비회원 비밀글은 틀린 비밀번호로 접근이 거부된다 (403)
*/
public function test_guest_secret_post_denied_with_wrong_password(): void
{
$postId = $this->createTestPost([
'is_secret' => true,
'password' => Hash::make('pass1234'),
'author_name' => '비회원',
'title' => '비회원 비밀글',
'content' => '비회원 비밀 내용입니다.',
]);
$response = $this->postJson(
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/verify-password",
['password' => 'wrongpass']
);
$response->assertStatus(403);
}
// ==========================================
// 비밀글 목록 노출 시나리오
// ==========================================
/**
* 비밀글은 목록에 포함되며 is_secret 플래그가 표시된다
*/
public function test_secret_posts_appear_in_list_with_masked_content(): void
{
$this->createTestPost(['is_secret' => false, 'title' => '공개글']);
$this->createTestPost(['is_secret' => true, 'title' => '비밀글', 'user_id' => $this->authorUser->id]);
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts");
$response->assertStatus(200);
$items = $response->json('data.data');
$this->assertCount(2, $items);
// 비밀글이 목록에 포함되어 있음 (is_secret 플래그)
$secretPost = collect($items)->firstWhere('is_secret', true);
$this->assertNotNull($secretPost);
}
/**
* secret_mode=always 게시판에서 navigation API는 500을 반환하지 않는다
*/
public function test_navigation_does_not_500_on_always_secret_board(): void
{
$this->updateBoardSettings(['secret_mode' => 'always']);
$postId = $this->createTestPost(['is_secret' => true, 'user_id' => $this->authorUser->id]);
$response = $this->getJson(
"/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/navigation"
);
// 500이 아닌 200 또는 404여야 함
$this->assertNotEquals(500, $response->status());
}
}