fix: address code review findings for bookmark feature (H1–L5)

Fix broken tests (H1), align interface signatures with implementations (H3),
remove dead code (H4), add missing input guard (M1), replace raw $_SESSION
access with SessionStoreInterface (M2), sync update script schema (M4),
use shared getAppBase utility (L2), document fragment stripping (L3),
and apply app- CSS class prefix convention (L5).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 22:58:07 +01:00
parent 9688848401
commit d9805c45d3
10 changed files with 195 additions and 91 deletions

View File

@@ -94,27 +94,17 @@ class BookmarkServiceTest extends TestCase
$this->assertSame('updated', $result['mode']);
}
public function testSaveBookmarkUpsertsLegacyBookmarkByCanonicalUrl(): void
public function testSaveBookmarkCreatesWhenNoExactUrlMatch(): void
{
$this->bookmarkRepo->expects($this->once())->method('findByUserAndUrl')->with(1, 'admin/users?a=1&b=2')->willReturn(false);
$this->bookmarkRepo->expects($this->once())->method('listByUser')->with(1)->willReturn([
['id' => 7, 'url' => 'admin/users?b=2&a=1'],
]);
$this->bookmarkRepo->expects($this->once())->method('update')->with(
7,
1,
[
'name' => 'Users',
'group_id' => null,
'url' => 'admin/users?a=1&b=2',
]
)->willReturn(true);
$this->bookmarkRepo->expects($this->never())->method('create');
$this->bookmarkRepo->method('countByUser')->willReturn(0);
$this->navigationRepo->method('nextRootSortOrder')->willReturn(1);
$this->bookmarkRepo->expects($this->once())->method('create')->willReturn(7);
$result = $this->service->saveBookmark(1, 'Users', 'admin/users?a=1&b=2', null);
$this->assertTrue($result['ok']);
$this->assertSame('updated', $result['mode']);
$this->assertSame('created', $result['mode']);
$this->assertSame(7, $result['bookmark']['id']);
}
@@ -338,22 +328,46 @@ class BookmarkServiceTest extends TestCase
$this->assertSame('bi-heart', $result['group']['icon']);
}
public function testDeleteGroupClearsBookmarks(): void
public function testDeleteGroupUnsetsGroupAndDeletes(): void
{
$this->bookmarkRepo->expects($this->once())->method('clearGroupId')->with(5, 1)->willReturn(true);
$this->groupRepo->expects($this->any())->method('delete')->with(5, 1)->willReturn(true);
$this->bookmarkRepo->expects($this->once())->method('listByGroup')->with(1, 5)->willReturn([
['id' => 10, 'name' => 'A', 'url' => 'admin', 'group_id' => 5, 'sort_order' => 1],
['id' => 11, 'name' => 'B', 'url' => 'admin/users', 'group_id' => 5, 'sort_order' => 2],
]);
$this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(20);
$this->bookmarkRepo->expects($this->exactly(2))->method('unsetGroupId')
->willReturnCallback(fn (int $bmId, int $userId, int $sort) => match ([$bmId, $userId]) {
[10, 1] => $sort === 20,
[11, 1] => $sort === 21,
default => false,
});
$this->groupRepo->expects($this->once())->method('delete')->with(5, 1)->willReturn(true);
$this->assertTrue($this->service->deleteGroup(1, 5));
}
public function testDeleteGroupReturnsFalseWhenClearFails(): void
public function testDeleteGroupReturnsFalseWhenUnsetFails(): void
{
$this->bookmarkRepo->expects($this->once())->method('clearGroupId')->with(5, 1)->willReturn(false);
$this->bookmarkRepo->expects($this->once())->method('listByGroup')->with(1, 5)->willReturn([
['id' => 10, 'name' => 'A', 'url' => 'admin', 'group_id' => 5, 'sort_order' => 1],
]);
$this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(20);
$this->bookmarkRepo->expects($this->once())->method('unsetGroupId')->with(10, 1, 20)->willReturn(false);
$this->groupRepo->expects($this->never())->method('delete');
$this->assertFalse($this->service->deleteGroup(1, 5));
}
public function testDeleteGroupWithNoBookmarksDeletesDirectly(): void
{
$this->bookmarkRepo->expects($this->once())->method('listByGroup')->with(1, 5)->willReturn([]);
$this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(1);
$this->bookmarkRepo->expects($this->never())->method('unsetGroupId');
$this->groupRepo->expects($this->once())->method('delete')->with(5, 1)->willReturn(true);
$this->assertTrue($this->service->deleteGroup(1, 5));
}
public function testReorderGroupsDelegatesToRepository(): void
{
$pairs = [
@@ -413,6 +427,60 @@ class BookmarkServiceTest extends TestCase
$this->assertArrayNotHasKey('icon', $result['ungrouped'][0]);
}
public function testReorderFromItemsRejectsInvalidType(): void
{
$result = $this->service->reorderFromItems(1, 'invalid', []);
$this->assertFalse($result['ok']);
$this->assertSame('invalid_type', $result['error']);
}
public function testReorderFromItemsRejectsEmptyItems(): void
{
$result = $this->service->reorderFromItems(1, 'bookmarks', []);
$this->assertFalse($result['ok']);
$this->assertSame('invalid_items', $result['error']);
}
public function testReorderFromItemsBookmarksHappyPath(): void
{
$this->bookmarkRepo->expects($this->once())->method('updateSortOrders')->with(1, [
['id' => 3, 'sort_order' => 1],
['id' => 7, 'sort_order' => 2],
])->willReturn(true);
$result = $this->service->reorderFromItems(1, 'bookmarks', [
['id' => 3, 'sort_order' => 1],
['id' => 7, 'sort_order' => 2],
]);
$this->assertTrue($result['ok']);
}
public function testReorderRootFromItemsHappyPath(): void
{
$this->navigationRepo->expects($this->once())->method('updateRootSortOrders')->with(1, [
['kind' => 'group', 'id' => 2, 'sort_order' => 1],
['kind' => 'bookmark', 'id' => 5, 'sort_order' => 2],
])->willReturn(true);
$result = $this->service->reorderRootFromItems(1, [
['kind' => 'group', 'id' => 2, 'sort_order' => 1],
['kind' => 'bookmark', 'id' => 5, 'sort_order' => 2],
]);
$this->assertTrue($result['ok']);
}
public function testReorderRootFromItemsRejectsEmptyItems(): void
{
$result = $this->service->reorderRootFromItems(1, []);
$this->assertFalse($result['ok']);
$this->assertSame('invalid_items', $result['error']);
}
public function testAllowedGroupIconsReturnsNonEmptyList(): void
{
$icons = BookmarkService::allowedGroupIcons();