Files
Gnuboard7/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php
T
2026-04-01 10:30:52 +09:00

504 lines
17 KiB
PHP

<?php
namespace Modules\Sirsoft\Page\Tests\Unit\Services;
use App\Enums\PermissionType;
use App\Enums\ScopeType;
use App\Helpers\PermissionHelper;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Modules\Sirsoft\Page\Models\Page;
use Modules\Sirsoft\Page\Models\PageVersion;
use Modules\Sirsoft\Page\Services\PageService;
use Modules\Sirsoft\Page\Tests\ModuleTestCase;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* PageService 단위 테스트
*
* 페이지 생성/수정/삭제/발행/버전 관리 비즈니스 로직을 검증합니다.
*/
class PageServiceTest extends ModuleTestCase
{
private PageService $service;
private User $adminUser;
protected function setUp(): void
{
parent::setUp();
$this->adminUser = $this->createAdminUser([]);
// Unit 테스트에서는 HTTP 요청 없이 Auth::id()를 사용하므로
// web guard와 sanctum guard 모두에 사용자를 설정
Auth::setUser($this->adminUser);
$this->actingAs($this->adminUser);
$this->service = app(PageService::class);
}
// ─── createPage ────────────────────────────────────
/**
* 페이지를 생성할 수 있는지 확인
*/
public function test_create_page_returns_page_model(): void
{
$data = [
'slug' => 'test-svc-create',
'title' => ['ko' => '서비스 테스트', 'en' => 'Service Test'],
'content' => ['ko' => '본문', 'en' => 'Content'],
'content_mode' => 'html',
];
$page = $this->service->createPage($data);
$this->assertInstanceOf(Page::class, $page);
$this->assertEquals('test-svc-create', $page->slug);
$this->assertEquals(1, $page->current_version);
}
/**
* 페이지 생성 시 버전 1 스냅샷이 자동 생성되는지 확인
*/
public function test_create_page_creates_version_one(): void
{
$page = $this->service->createPage([
'slug' => 'test-svc-version',
'title' => ['ko' => '버전 테스트', 'en' => 'Version'],
'content' => ['ko' => '본문', 'en' => 'Body'],
'content_mode' => 'html',
]);
$versions = PageVersion::where('page_id', $page->id)->get();
$this->assertCount(1, $versions);
$this->assertEquals(1, $versions->first()->version);
}
/**
* 발행 상태로 생성 시 published_at이 설정되는지 확인
*/
public function test_create_published_page_sets_published_at(): void
{
$page = $this->service->createPage([
'slug' => 'test-svc-pub',
'title' => ['ko' => '발행', 'en' => 'Published'],
'content' => ['ko' => '본문', 'en' => 'Body'],
'content_mode' => 'html',
'published' => true,
]);
$this->assertTrue($page->published);
$this->assertNotNull($page->published_at);
}
// ─── updatePage ────────────────────────────────────
/**
* 페이지를 수정할 수 있는지 확인
*/
public function test_update_page_changes_title(): void
{
$page = $this->createTestPage('test-svc-update');
$updated = $this->service->updatePage($page, [
'title' => ['ko' => '수정됨', 'en' => 'Updated'],
]);
$this->assertEquals('수정됨', $updated->title['ko']);
}
/**
* 수정 시 버전 번호가 증가하는지 확인
*/
public function test_update_page_increments_version(): void
{
$page = $this->createTestPage('test-svc-ver-inc');
$originalVersion = $page->current_version;
$updated = $this->service->updatePage($page, [
'title' => ['ko' => '버전 증가', 'en' => 'Version Up'],
]);
$this->assertEquals($originalVersion + 1, $updated->current_version);
}
/**
* 수정 시 새 버전 스냅샷이 생성되는지 확인
*/
public function test_update_page_creates_new_version_snapshot(): void
{
$page = $this->createTestPage('test-svc-snap');
$this->service->updatePage($page, [
'title' => ['ko' => '스냅샷', 'en' => 'Snapshot'],
]);
$versionCount = PageVersion::where('page_id', $page->id)->count();
$this->assertEquals(2, $versionCount); // 생성 시 1 + 수정 시 1
}
// ─── deletePage ────────────────────────────────────
/**
* 페이지를 소프트 삭제할 수 있는지 확인
*/
public function test_delete_page_soft_deletes(): void
{
$page = $this->createTestPage('test-svc-delete');
$result = $this->service->deletePage($page);
$this->assertTrue($result);
$this->assertSoftDeleted('pages', ['id' => $page->id]);
}
// ─── changePublishStatus ───────────────────────────
/**
* 발행 상태를 변경할 수 있는지 확인
*/
public function test_change_publish_status_to_published(): void
{
$page = $this->createTestPage('test-svc-publish');
$published = $this->service->changePublishStatus($page, true);
$this->assertTrue($published->published);
$this->assertNotNull($published->published_at);
}
/**
* 미발행 상태로 변경할 수 있는지 확인
*/
public function test_change_publish_status_to_unpublished(): void
{
$page = Page::factory()->published()->create([
'slug' => 'test-svc-unpub',
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$unpublished = $this->service->changePublishStatus($page, false);
$this->assertFalse($unpublished->published);
}
// ─── bulkChangePublishStatus ───────────────────────
/**
* 여러 페이지를 일괄 발행할 수 있는지 확인
*/
public function test_bulk_change_publish_status(): void
{
$pages = Page::factory()->count(3)->create([
'published' => false,
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$count = $this->service->bulkChangePublishStatus(
$pages->pluck('id')->toArray(),
true
);
$this->assertEquals(3, $count);
}
// ─── restoreVersion ────────────────────────────────
/**
* 특정 버전으로 복원할 수 있는지 확인
*/
public function test_restore_version_reverts_content(): void
{
$page = $this->createTestPage('test-svc-restore');
$originalTitle = $page->title;
// 수정하여 버전 2 생성
$page = $this->service->updatePage($page, [
'title' => ['ko' => '변경된 제목', 'en' => 'Changed Title'],
]);
// 버전 1 가져오기
$version1 = PageVersion::where('page_id', $page->id)
->where('version', 1)
->first();
// 버전 1로 복원
$restored = $this->service->restoreVersion($page, $version1->id);
$this->assertEquals($originalTitle['ko'], $restored->title['ko']);
$this->assertEquals(3, $restored->current_version); // 복원 후 버전 증가
}
// ─── slugExists ────────────────────────────────────
/**
* 슬러그 존재 여부를 확인할 수 있는지 검증
*/
public function test_slug_exists_returns_true_for_existing_slug(): void
{
$this->createTestPage('test-svc-slug-exist');
$this->assertTrue($this->service->slugExists('test-svc-slug-exist'));
}
/**
* 존재하지 않는 슬러그는 false를 반환하는지 확인
*/
public function test_slug_exists_returns_false_for_nonexistent(): void
{
$this->assertFalse($this->service->slugExists('test-svc-no-slug'));
}
/**
* excludeId로 자기 자신을 제외할 수 있는지 확인
*/
public function test_slug_exists_excludes_own_id(): void
{
$page = $this->createTestPage('test-svc-exclude');
$this->assertFalse($this->service->slugExists('test-svc-exclude', $page->id));
}
// ─── getPublishedPageBySlug ────────────────────────
/**
* 발행된 페이지를 슬러그로 조회할 수 있는지 확인
*/
public function test_get_published_page_by_slug(): void
{
Page::factory()->published()->create([
'slug' => 'test-svc-pub-slug',
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$page = $this->service->getPublishedPageBySlug('test-svc-pub-slug');
$this->assertNotNull($page);
$this->assertEquals('test-svc-pub-slug', $page->slug);
}
/**
* 미발행 페이지는 슬러그로 조회할 수 없는지 확인
*/
public function test_get_published_page_by_slug_returns_null_for_draft(): void
{
Page::factory()->create([
'slug' => 'test-svc-draft-slug',
'published' => false,
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$page = $this->service->getPublishedPageBySlug('test-svc-draft-slug');
$this->assertNull($page);
}
// ─── scope 검증 (AccessDeniedHttpException) ────
/**
* getPage()에서 scope=self일 때 타인의 페이지 조회 시 AccessDeniedHttpException을 던지는지 확인
*/
public function test_get_page_throws_exception_for_scope_denied(): void
{
// scope=self 사용자 설정
$scopeUser = $this->setupScopeUser(ScopeType::Self);
Auth::setUser($scopeUser);
$this->actingAs($scopeUser);
// 다른 사용자가 만든 페이지
$page = Page::factory()->create([
'slug' => 'test-svc-scope-show',
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$this->expectException(AccessDeniedHttpException::class);
$this->service->getPage($page->id);
}
/**
* getPage()에서 scope=self일 때 자기 페이지는 정상 조회되는지 확인
*/
public function test_get_page_allows_own_page_with_scope_self(): void
{
$scopeUser = $this->setupScopeUser(ScopeType::Self);
Auth::setUser($scopeUser);
$this->actingAs($scopeUser);
$page = Page::factory()->create([
'slug' => 'test-svc-scope-show-own',
'created_by' => $scopeUser->id,
'updated_by' => $scopeUser->id,
]);
$result = $this->service->getPage($page->id);
$this->assertEquals($page->id, $result->id);
}
/**
* updatePage()에서 scope=self일 때 타인의 페이지 수정 시 AccessDeniedHttpException을 던지는지 확인
*/
public function test_update_page_throws_exception_for_scope_denied(): void
{
$scopeUser = $this->setupScopeUser(ScopeType::Self);
Auth::setUser($scopeUser);
$this->actingAs($scopeUser);
$page = Page::factory()->create([
'slug' => 'test-svc-scope-update',
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$this->expectException(AccessDeniedHttpException::class);
$this->service->updatePage($page, [
'title' => ['ko' => '수정 시도', 'en' => 'Attempt'],
]);
}
/**
* deletePage()에서 scope=self일 때 타인의 페이지 삭제 시 AccessDeniedHttpException을 던지는지 확인
*/
public function test_delete_page_throws_exception_for_scope_denied(): void
{
$scopeUser = $this->setupScopeUser(ScopeType::Self);
Auth::setUser($scopeUser);
$this->actingAs($scopeUser);
$page = Page::factory()->create([
'slug' => 'test-svc-scope-delete',
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$this->expectException(AccessDeniedHttpException::class);
$this->service->deletePage($page);
}
/**
* changePublishStatus()에서 scope=self일 때 타인의 페이지 발행 변경 시 AccessDeniedHttpException을 던지는지 확인
*/
public function test_change_publish_status_throws_exception_for_scope_denied(): void
{
$scopeUser = $this->setupScopeUser(ScopeType::Self);
Auth::setUser($scopeUser);
$this->actingAs($scopeUser);
$page = Page::factory()->create([
'slug' => 'test-svc-scope-publish',
'published' => false,
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$this->expectException(AccessDeniedHttpException::class);
$this->service->changePublishStatus($page, true);
}
/**
* restoreVersion()에서 scope=self일 때 타인의 페이지 버전 복원 시 AccessDeniedHttpException을 던지는지 확인
*/
public function test_restore_version_throws_exception_for_scope_denied(): void
{
$scopeUser = $this->setupScopeUser(ScopeType::Self);
Auth::setUser($scopeUser);
$this->actingAs($scopeUser);
$page = Page::factory()->create([
'slug' => 'test-svc-scope-restore',
'current_version' => 1,
'created_by' => $this->adminUser->id,
'updated_by' => $this->adminUser->id,
]);
$version = PageVersion::create([
'page_id' => $page->id,
'version' => 1,
'title' => $page->title,
'content' => $page->content ?? ['ko' => '', 'en' => ''],
'content_mode' => 'html',
'created_by' => $this->adminUser->id,
]);
$this->expectException(AccessDeniedHttpException::class);
$this->service->restoreVersion($page, $version->id);
}
// ─── 헬퍼 ────────────────────────────────────────
/**
* 테스트용 페이지를 생성합니다.
*/
private function createTestPage(string $slug): Page
{
return $this->service->createPage([
'slug' => $slug,
'title' => ['ko' => '테스트 페이지', 'en' => 'Test Page'],
'content' => ['ko' => '테스트 본문', 'en' => 'Test Content'],
'content_mode' => 'html',
]);
}
/**
* scope 권한이 설정된 사용자를 생성합니다.
*
* @param ScopeType $scopeType 스코프 타입
* @return User 생성된 사용자
*/
private function setupScopeUser(ScopeType $scopeType): User
{
$user = User::factory()->create();
$role = Role::firstOrCreate(
['identifier' => 'test_page_svc_scope'],
[
'name' => ['ko' => '페이지 서비스 스코프', 'en' => 'Page Service Scope'],
'is_active' => true,
]
);
$user->roles()->attach($role->id);
$permissionIds = [
'sirsoft-page.pages.read',
'sirsoft-page.pages.create',
'sirsoft-page.pages.update',
'sirsoft-page.pages.delete',
];
foreach ($permissionIds as $identifier) {
$permission = Permission::firstOrCreate(
['identifier' => $identifier],
[
'name' => ['ko' => $identifier, 'en' => $identifier],
'type' => PermissionType::Admin,
]
);
$permission->update([
'resource_route_key' => 'page',
'owner_key' => 'created_by',
]);
$role->permissions()->syncWithoutDetaching([
$permission->id => ['scope_type' => $scopeType],
]);
}
// PermissionHelper 캐시 초기화
$reflection = new \ReflectionClass(PermissionHelper::class);
$prop = $reflection->getProperty('permissionCache');
$prop->setAccessible(true);
$prop->setValue(null, []);
return $user;
}
}