|
25 | 25 | use OCA\Bookmarks\Service\TreeCacheManager; |
26 | 26 | use OCP\AppFramework\Db\MultipleObjectsReturnedException; |
27 | 27 | use OCP\AppFramework\QueryException; |
| 28 | +use OCP\DB\QueryBuilder\IQueryBuilder; |
28 | 29 | use OCP\ICache; |
29 | 30 | use OCP\ICacheFactory; |
| 31 | +use OCP\IDBConnection; |
30 | 32 | use OCP\IGroupManager; |
31 | 33 | use OCP\IRequest; |
32 | 34 | use OCP\IUserManager; |
@@ -257,6 +259,51 @@ public function testRead(): void { |
257 | 259 | $this->assertEquals($this->folder1->getTitle(), $data['item']['title']); |
258 | 260 | } |
259 | 261 |
|
| 262 | + /** |
| 263 | + * Regression test: trashed folders must not be listed more than once. |
| 264 | + * |
| 265 | + * The id columns of bookmarks, bookmarks_folders and bookmarks_shared_folders |
| 266 | + * are independent autoincrement sequences, so the same numeric id routinely |
| 267 | + * exists across all three types. getSoftDeletedRootItems() self-joins |
| 268 | + * bookmarks_tree to inspect each item's parent folder; when that join lacked |
| 269 | + * a type constraint, a bookmark or share sharing the parent folder's id |
| 270 | + * matched too, multiplying the result rows and showing every folder twice in |
| 271 | + * the trash bin. |
| 272 | + * |
| 273 | + * @throws AlreadyExistsError |
| 274 | + * @throws UrlParseError |
| 275 | + * @throws UserLimitExceededError |
| 276 | + * @throws MultipleObjectsReturnedException |
| 277 | + * @throws \OCP\AppFramework\Db\DoesNotExistException |
| 278 | + */ |
| 279 | + public function testGetSoftDeletedRootItemsDoesNotDuplicateOnIdCollision(): void { |
| 280 | + $this->setupBookmarks(); |
| 281 | + $this->authorizer->setUserId($this->userId); |
| 282 | + |
| 283 | + // folder2 lives inside folder1; trashing it individually makes it a root |
| 284 | + // item of the trash bin, since its parent (folder1) is not trashed. |
| 285 | + $this->treeMapper->softDeleteEntry(TreeMapper::TYPE_FOLDER, $this->folder2->getId()); |
| 286 | + |
| 287 | + // Reproduce a cross-type id collision: a non-folder tree entry whose id |
| 288 | + // equals the trashed folder's parent (folder1). This is the data shape |
| 289 | + // that used to duplicate rows via the type-less parent self-join. |
| 290 | + $db = \OCP\Server::get(IDBConnection::class); |
| 291 | + $qb = $db->getQueryBuilder(); |
| 292 | + $qb->insert('bookmarks_tree') |
| 293 | + ->values([ |
| 294 | + 'id' => $qb->createNamedParameter($this->folder1->getId(), IQueryBuilder::PARAM_INT), |
| 295 | + 'type' => $qb->createNamedParameter(TreeMapper::TYPE_BOOKMARK), |
| 296 | + 'parent_folder' => $qb->createNamedParameter($this->folderMapper->findRootFolder($this->userId)->getId(), IQueryBuilder::PARAM_INT), |
| 297 | + 'index' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), |
| 298 | + ]); |
| 299 | + $qb->executeStatement(); |
| 300 | + |
| 301 | + $rootItems = $this->treeMapper->getSoftDeletedRootItems($this->userId, TreeMapper::TYPE_FOLDER); |
| 302 | + |
| 303 | + $ids = array_map(static fn ($folder) => $folder->getId(), $rootItems); |
| 304 | + $this->assertEquals([$this->folder2->getId()], $ids, 'Trashed folder must appear exactly once'); |
| 305 | + } |
| 306 | + |
260 | 307 | /** |
261 | 308 | * @throws AlreadyExistsError |
262 | 309 | * @throws UrlParseError |
|
0 commit comments