2020use OCA \GroupFolders \Folder \FolderManager ;
2121use OCA \GroupFolders \Mount \GroupFolderStorage ;
2222use OCA \GroupFolders \Trash \TrashBackend ;
23+ use OCA \GroupFolders \Trash \TrashManager ;
2324use OCP \Constants ;
2425use OCP \Files \Folder ;
2526use OCP \Files \IRootFolder ;
@@ -37,6 +38,7 @@ class TrashBackendTest extends TestCase {
3738
3839 private string $ folderName ;
3940 private TrashBackend $ trashBackend ;
41+ private TrashManager $ trashManager ;
4042 private FolderManager $ folderManager ;
4143 private ACLManager $ aclManager ;
4244 private RuleManager $ ruleManager ;
@@ -62,6 +64,7 @@ public function setUp(): void {
6264 $ groupBackend ->addToGroup ('normal ' , 'gf_normal ' );
6365
6466 $ this ->trashBackend = \OCP \Server::get (TrashBackend::class);
67+ $ this ->trashManager = \OCP \Server::get (TrashManager::class);
6568 $ this ->folderManager = \OCP \Server::get (FolderManager::class);
6669 /** @var ACLManagerFactory $aclManagerFactory */
6770 $ aclManagerFactory = \OCP \Server::get (ACLManagerFactory::class);
@@ -291,4 +294,48 @@ public function testWrongOriginalLocation(): void {
291294 $ this ->trashBackend ->restoreItem ($ trashedOfUserA [0 ]);
292295 $ this ->assertTrue ($ userAFolder ->nodeExists ('D/E/F/G ' ));
293296 }
297+
298+ public function testMoveToTrashSameNameDifferentSubfolders (): void {
299+ $ this ->loginAsUser ('manager ' );
300+
301+ // Create two subfolders each containing a file with the same name
302+ $ folderA = $ this ->managerUserFolder ->newFolder ("{$ this ->folderName }/SubA " );
303+ $ folderB = $ this ->managerUserFolder ->newFolder ("{$ this ->folderName }/SubB " );
304+ $ fileA = $ folderA ->newFile ('Readme.md ' , 'content A ' );
305+ $ fileB = $ folderB ->newFile ('Readme.md ' , 'content B ' );
306+
307+ $ this ->assertTrue ($ this ->managerUserFolder ->nodeExists ("{$ this ->folderName }/SubA/Readme.md " ));
308+ $ this ->assertTrue ($ this ->managerUserFolder ->nodeExists ("{$ this ->folderName }/SubB/Readme.md " ));
309+
310+ // Wait for the start of a new second so both deletes land in the same
311+ // second and deterministically exercise the retry path.
312+ $ now = time ();
313+ while (time () === $ now ) {
314+ usleep (10000 );
315+ }
316+
317+ // Delete both files β they share the same base name within the same
318+ // group folder which previously triggered a unique constraint violation
319+ // on (folder_id, name, deleted_time) when both deletes hit the same second.
320+ $ this ->trashBackend ->moveToTrash ($ fileA ->getStorage (), $ fileA ->getInternalPath ());
321+ $ this ->trashBackend ->moveToTrash ($ fileB ->getStorage (), $ fileB ->getInternalPath ());
322+
323+ $ this ->assertFalse ($ this ->managerUserFolder ->nodeExists ("{$ this ->folderName }/SubA/Readme.md " ));
324+ $ this ->assertFalse ($ this ->managerUserFolder ->nodeExists ("{$ this ->folderName }/SubB/Readme.md " ));
325+
326+ // Both files must appear in the trash
327+ $ trashItems = $ this ->trashBackend ->listTrashRoot ($ this ->managerUser );
328+ $ this ->assertCount (2 , $ trashItems , 'Both Readme.md files should be in the trash ' );
329+
330+ // The two trash entries must have distinct deleted_time values
331+ $ times = array_map (fn ($ item ) => $ item ->getDeletedTime (), $ trashItems );
332+ $ this ->assertCount (2 , array_unique ($ times ), 'Trash entries should have distinct deleted_time values ' );
333+
334+ // Verify the DB records exist
335+ $ dbRows = $ this ->trashManager ->listTrashForFolders ([$ this ->folderId ]);
336+ $ dbReadmeRows = array_values (array_filter ($ dbRows , fn ($ row ) => $ row ['name ' ] === 'Readme.md ' ));
337+ $ this ->assertCount (2 , $ dbReadmeRows , 'Both Readme.md entries should exist in oc_group_folders_trash ' );
338+
339+ $ this ->logout ();
340+ }
294341}
0 commit comments