From 331e32968df02efa2894af73406d7e740d82800c Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Fri, 19 Sep 2025 11:47:09 +0200 Subject: [PATCH 1/7] Fix: Add cleanup for orphaned text steps Signed-off-by: Benjamin Frueh --- lib/Cron/Cleanup.php | 4 ++++ lib/Db/SessionMapper.php | 40 ++++++++++++++++++++++++++++++++++ lib/Service/SessionService.php | 4 ++++ 3 files changed, 48 insertions(+) diff --git a/lib/Cron/Cleanup.php b/lib/Cron/Cleanup.php index b7f29f6a4e7..eb749c6da30 100644 --- a/lib/Cron/Cleanup.php +++ b/lib/Cron/Cleanup.php @@ -58,6 +58,10 @@ protected function run($argument): void { $removedOldSessions = $this->sessionService->removeOldSessions(); $this->logger->debug('Removed ' . $removedOldSessions . ' old sessions'); + $this->logger->debug('Run cleanup job for orphaned steps'); + $removedSteps = $this->sessionService->removeOrphanedSteps(); + $this->logger->debug('Removed ' . $removedSteps . ' orphaned steps'); + $this->logger->debug('Run cleanup job for obsolete documents folders'); $this->documentService->cleanupOldDocumentsFolders(); } diff --git a/lib/Db/SessionMapper.php b/lib/Db/SessionMapper.php index cfded3e9a96..a8fa03ceb6a 100644 --- a/lib/Db/SessionMapper.php +++ b/lib/Db/SessionMapper.php @@ -178,6 +178,46 @@ public function deleteOldSessions(int $ageInSeconds): int { return $deletedCount; } + public function deleteOrphanedSteps(): int { + $startTime = microtime(true); + $maxExecutionSeconds = 30; + $batchSize = 1000; + $deletedCount = 0; + + do { + try { + $orphanedStepsBuilder = $this->db->getQueryBuilder(); + $orphanedStepsBuilder->select('st.id') + ->from('text_steps', 'st') + ->leftJoin('st', 'text_sessions', 's', $orphanedStepsBuilder->expr()->eq('st.session_id', 's.id')) + ->where($orphanedStepsBuilder->expr()->isNull('s.id')) + ->setMaxResults($batchSize); + + $result = $orphanedStepsBuilder->executeQuery(); + $stepIds = array_map(function ($row) { + return (int)$row['id']; + }, $result->fetchAll()); + $result->closeCursor(); + + if (empty($stepIds)) { + break; + } + + $deleteBuilder = $this->db->getQueryBuilder(); + $batchDeleted = $deleteBuilder->delete('text_steps') + ->where($deleteBuilder->expr()->in('id', $deleteBuilder->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)) + ->setParameter('ids', $stepIds, IQueryBuilder::PARAM_INT_ARRAY) + ->executeStatement(); + + $deletedCount += $batchDeleted; + } catch (\Exception) { + break; + } + } while ((microtime(true) - $startTime) < $maxExecutionSeconds); + + return $deletedCount; + } + public function deleteByDocumentId(int $documentId): int { $qb = $this->db->getQueryBuilder(); $qb->delete($this->getTableName()) diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index 338e91e721c..b45648fb4d6 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -142,6 +142,10 @@ public function removeOldSessions(int $ageInSeconds = 7776000): int { return $this->sessionMapper->deleteOldSessions($ageInSeconds); } + public function removeOrphanedSteps(): int { + return $this->sessionMapper->deleteOrphanedSteps(); + } + public function getSession(int $documentId, int $sessionId, string $token): ?Session { if ($this->session !== null) { return $this->session; From f95e52f2561896e310386d03eb94da2f721786e9 Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Sat, 20 Sep 2025 22:51:31 +0200 Subject: [PATCH 2/7] Remove try-catch from deleteOrphanedSteps for consistency Signed-off-by: Benjamin Frueh --- lib/Db/SessionMapper.php | 48 ++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/lib/Db/SessionMapper.php b/lib/Db/SessionMapper.php index a8fa03ceb6a..57d5a91a6db 100644 --- a/lib/Db/SessionMapper.php +++ b/lib/Db/SessionMapper.php @@ -185,34 +185,30 @@ public function deleteOrphanedSteps(): int { $deletedCount = 0; do { - try { - $orphanedStepsBuilder = $this->db->getQueryBuilder(); - $orphanedStepsBuilder->select('st.id') - ->from('text_steps', 'st') - ->leftJoin('st', 'text_sessions', 's', $orphanedStepsBuilder->expr()->eq('st.session_id', 's.id')) - ->where($orphanedStepsBuilder->expr()->isNull('s.id')) - ->setMaxResults($batchSize); - - $result = $orphanedStepsBuilder->executeQuery(); - $stepIds = array_map(function ($row) { - return (int)$row['id']; - }, $result->fetchAll()); - $result->closeCursor(); - - if (empty($stepIds)) { - break; - } - - $deleteBuilder = $this->db->getQueryBuilder(); - $batchDeleted = $deleteBuilder->delete('text_steps') - ->where($deleteBuilder->expr()->in('id', $deleteBuilder->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)) - ->setParameter('ids', $stepIds, IQueryBuilder::PARAM_INT_ARRAY) - ->executeStatement(); - - $deletedCount += $batchDeleted; - } catch (\Exception) { + $orphanedStepsBuilder = $this->db->getQueryBuilder(); + $orphanedStepsBuilder->select('st.id') + ->from('text_steps', 'st') + ->leftJoin('st', 'text_sessions', 's', $orphanedStepsBuilder->expr()->eq('st.session_id', 's.id')) + ->where($orphanedStepsBuilder->expr()->isNull('s.id')) + ->setMaxResults($batchSize); + + $result = $orphanedStepsBuilder->executeQuery(); + $stepIds = array_map(function ($row) { + return (int)$row['id']; + }, $result->fetchAll()); + $result->closeCursor(); + + if (empty($stepIds)) { break; } + + $deleteBuilder = $this->db->getQueryBuilder(); + $batchDeleted = $deleteBuilder->delete('text_steps') + ->where($deleteBuilder->expr()->in('id', $deleteBuilder->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)) + ->setParameter('ids', $stepIds, IQueryBuilder::PARAM_INT_ARRAY) + ->executeStatement(); + + $deletedCount += $batchDeleted; } while ((microtime(true) - $startTime) < $maxExecutionSeconds); return $deletedCount; From abf3780f94a50ff14395d15c7ea12b206839747a Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Sat, 20 Sep 2025 23:23:15 +0200 Subject: [PATCH 3/7] Add test for deleteOrphanedSteps method Signed-off-by: Benjamin Frueh --- tests/unit/Db/SessionMapperTest.php | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/unit/Db/SessionMapperTest.php b/tests/unit/Db/SessionMapperTest.php index 8d3882617e8..f0e28bd9be3 100644 --- a/tests/unit/Db/SessionMapperTest.php +++ b/tests/unit/Db/SessionMapperTest.php @@ -135,4 +135,61 @@ public function testDeleteOldSessions() { self::assertCount(1, $remainingSessions); self::assertEquals($recentSession->getId(), $remainingSessions[0]->getId()); } + + public function testDeleteOrphanedSteps() { + $this->stepMapper->deleteAll(1); + $this->sessionMapper->deleteByDocumentId(1); + + // Create session + $session = $this->sessionMapper->insert(Session::fromParams([ + 'userId' => 'admin', + 'documentId' => 1, + 'lastContact' => time(), + 'token' => uniqid(), + 'color' => '00ff00', + ])); + + // Create steps for session + $this->stepMapper->insert(Step::fromParams([ + 'sessionId' => $session->getId(), + 'documentId' => 1, + 'data' => 'YJSDATA1', + 'version' => 1, + ])); + $this->stepMapper->insert(Step::fromParams([ + 'sessionId' => $session->getId(), + 'documentId' => 1, + 'data' => 'YJSDATA2', + 'version' => 2, + ])); + + // Create orphaned steps + $this->stepMapper->insert(Step::fromParams([ + 'sessionId' => 99999, // Non-existent session + 'documentId' => 1, + 'data' => 'ORPHANED1', + 'version' => 3, + ])); + $this->stepMapper->insert(Step::fromParams([ + 'sessionId' => 99998, // Non-existent session + 'documentId' => 1, + 'data' => 'ORPHANED2', + 'version' => 4, + ])); + + // Verify 4 steps total + $allSteps = $this->stepMapper->find(1, 0); + self::assertCount(4, $allSteps); + + // Delete orphaned steps + $deletedCount = $this->sessionMapper->deleteOrphanedSteps(); + + // Should have deleted 2 orphaned steps + self::assertEquals(2, $deletedCount); + + // Should have 2 valid steps remaining + $remainingSteps = $this->stepMapper->find(1, 0); + self::assertCount(2, $remainingSteps); +>>>>>>> a66cd75d2 (Add test for deleteOrphanedSteps method) + } } From 33657d202723096c91157ee56a8a923d4c2c860a Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Mon, 22 Sep 2025 14:58:44 +0200 Subject: [PATCH 4/7] Changed deleteOrphanedSteps to respect document versioning and added a safety buffer Signed-off-by: Benjamin Frueh --- lib/Db/SessionMapper.php | 25 ++++++--- tests/unit/Db/SessionMapperTest.php | 87 +++++++++++++++-------------- 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/lib/Db/SessionMapper.php b/lib/Db/SessionMapper.php index 57d5a91a6db..c61a97ef8b1 100644 --- a/lib/Db/SessionMapper.php +++ b/lib/Db/SessionMapper.php @@ -183,16 +183,25 @@ public function deleteOrphanedSteps(): int { $maxExecutionSeconds = 30; $batchSize = 1000; $deletedCount = 0; + $safetyBufferTime = time() - 86400; do { - $orphanedStepsBuilder = $this->db->getQueryBuilder(); - $orphanedStepsBuilder->select('st.id') + $orphanedStepsQb = $this->db->getQueryBuilder(); + $orphanedStepsQb->select('st.id') ->from('text_steps', 'st') - ->leftJoin('st', 'text_sessions', 's', $orphanedStepsBuilder->expr()->eq('st.session_id', 's.id')) - ->where($orphanedStepsBuilder->expr()->isNull('s.id')) + ->leftJoin('st', 'text_sessions', 's', $orphanedStepsQb->expr()->eq('st.session_id', 's.id')) + ->leftJoin('st', 'text_documents', 'd', $orphanedStepsQb->expr()->eq('st.document_id', 'd.id')) + ->where($orphanedStepsQb->expr()->isNull('s.id')) + ->andWhere($orphanedStepsQb->expr()->lt('st.timestamp', $orphanedStepsQb->createNamedParameter($safetyBufferTime))) + ->andWhere( + $orphanedStepsQb->expr()->orX( + $orphanedStepsQb->expr()->isNull('d.id'), + $orphanedStepsQb->expr()->lt('st.id', 'd.last_saved_version') + ) + ) ->setMaxResults($batchSize); - $result = $orphanedStepsBuilder->executeQuery(); + $result = $orphanedStepsQb->executeQuery(); $stepIds = array_map(function ($row) { return (int)$row['id']; }, $result->fetchAll()); @@ -202,9 +211,9 @@ public function deleteOrphanedSteps(): int { break; } - $deleteBuilder = $this->db->getQueryBuilder(); - $batchDeleted = $deleteBuilder->delete('text_steps') - ->where($deleteBuilder->expr()->in('id', $deleteBuilder->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)) + $deleteQb = $this->db->getQueryBuilder(); + $batchDeleted = $deleteQb->delete('text_steps') + ->where($deleteQb->expr()->in('id', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)) ->setParameter('ids', $stepIds, IQueryBuilder::PARAM_INT_ARRAY) ->executeStatement(); diff --git a/tests/unit/Db/SessionMapperTest.php b/tests/unit/Db/SessionMapperTest.php index f0e28bd9be3..3e0ac33ae24 100644 --- a/tests/unit/Db/SessionMapperTest.php +++ b/tests/unit/Db/SessionMapperTest.php @@ -9,12 +9,13 @@ class SessionMapperTest extends \Test\TestCase { private SessionMapper $sessionMapper; private StepMapper $stepMapper; + private DocumentMapper $documentMapper; public function setUp(): void { parent::setUp(); $this->sessionMapper = \OCP\Server::get(SessionMapper::class); $this->stepMapper = \OCP\Server::get(StepMapper::class); - + $this->documentMapper = \OCP\Server::get(DocumentMapper::class); } public function testDeleteInactiveWithoutSteps() { @@ -137,59 +138,63 @@ public function testDeleteOldSessions() { } public function testDeleteOrphanedSteps() { - $this->stepMapper->deleteAll(1); - $this->sessionMapper->deleteByDocumentId(1); - - // Create session - $session = $this->sessionMapper->insert(Session::fromParams([ - 'userId' => 'admin', - 'documentId' => 1, - 'lastContact' => time(), - 'token' => uniqid(), - 'color' => '00ff00', + $this->documentMapper->clearAll(); + $this->sessionMapper->clearAll(); + $this->stepMapper->clearAll(); + + $oldTimestamp = time() - 86401; + + // Create document + $document = $this->documentMapper->insert(Document::fromParams([ + 'id' => 1, + 'currentVersion' => 0, + 'lastSavedVersion' => 100, + 'lastSavedVersionTime' => time() ])); - // Create steps for session + // Create Orphaned step without document (delete) $this->stepMapper->insert(Step::fromParams([ - 'sessionId' => $session->getId(), - 'documentId' => 1, - 'data' => 'YJSDATA1', - 'version' => 1, - ])); + 'sessionId' => 99999, + 'documentId' => 99999, + 'data' => 'ORPHANED_NO_DOC', + 'version' => 1 + ])); + + // Orphaned "old" step with document and old version (delete) $this->stepMapper->insert(Step::fromParams([ - 'sessionId' => $session->getId(), - 'documentId' => 1, - 'data' => 'YJSDATA2', - 'version' => 2, + 'id' => 1, + 'sessionId' => 99999, + 'documentId' => $document->getId(), + 'data' => 'ORPHANED_OLD_VERSION', + 'timestamp' => $oldTimestamp, + 'version' => 1 ])); - // Create orphaned steps + // Orphaned "new" step with document and current version (keep) $this->stepMapper->insert(Step::fromParams([ - 'sessionId' => 99999, // Non-existent session - 'documentId' => 1, - 'data' => 'ORPHANED1', - 'version' => 3, + 'id' => 100, + 'sessionId' => 99999, + 'documentId' => $document->getId(), + 'data' => 'ORPHANED_CURRENT_VERSION', + 'version' => 2 ])); + + // Orphaned step with document and new version (keep) $this->stepMapper->insert(Step::fromParams([ - 'sessionId' => 99998, // Non-existent session - 'documentId' => 1, - 'data' => 'ORPHANED2', - 'version' => 4, + 'id' => 101, + 'sessionId' => 99999, + 'documentId' => $document->getId(), + 'data' => 'ORPHANED_NEW_VERSION', + 'timestamp' => $oldTimestamp, + 'version' => 3 ])); - // Verify 4 steps total - $allSteps = $this->stepMapper->find(1, 0); - self::assertCount(4, $allSteps); + // Verify steps for document 1 and 99999 + self::assertCount(3, $this->stepMapper->find(1, 0)); + self::assertCount(1, $this->stepMapper->find(99999, 0)); - // Delete orphaned steps + // Verify orphan delete $deletedCount = $this->sessionMapper->deleteOrphanedSteps(); - - // Should have deleted 2 orphaned steps self::assertEquals(2, $deletedCount); - - // Should have 2 valid steps remaining - $remainingSteps = $this->stepMapper->find(1, 0); - self::assertCount(2, $remainingSteps); ->>>>>>> a66cd75d2 (Add test for deleteOrphanedSteps method) } } From 81a96c9383cd712d76aca8b026185140453733f0 Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Mon, 22 Sep 2025 14:59:05 +0200 Subject: [PATCH 5/7] Changed deleteOrphanedSteps to respect document versioning and added a safety buffer Signed-off-by: Benjamin Frueh --- tests/unit/Db/SessionMapperTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/Db/SessionMapperTest.php b/tests/unit/Db/SessionMapperTest.php index 3e0ac33ae24..ce673bd62ba 100644 --- a/tests/unit/Db/SessionMapperTest.php +++ b/tests/unit/Db/SessionMapperTest.php @@ -155,17 +155,17 @@ public function testDeleteOrphanedSteps() { // Create Orphaned step without document (delete) $this->stepMapper->insert(Step::fromParams([ 'sessionId' => 99999, - 'documentId' => 99999, + 'documentId' => 99999, 'data' => 'ORPHANED_NO_DOC', 'version' => 1 - ])); - + ])); + // Orphaned "old" step with document and old version (delete) $this->stepMapper->insert(Step::fromParams([ 'id' => 1, 'sessionId' => 99999, 'documentId' => $document->getId(), - 'data' => 'ORPHANED_OLD_VERSION', + 'data' => 'ORPHANED_OLD_VERSION', 'timestamp' => $oldTimestamp, 'version' => 1 ])); @@ -184,7 +184,7 @@ public function testDeleteOrphanedSteps() { 'id' => 101, 'sessionId' => 99999, 'documentId' => $document->getId(), - 'data' => 'ORPHANED_NEW_VERSION', + 'data' => 'ORPHANED_NEW_VERSION', 'timestamp' => $oldTimestamp, 'version' => 3 ])); From 6647cc7ee1bebb40761bd024e840105046cb660d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Fr=C3=BCh?= <134610227+benjaminfrueh@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:54:19 +0200 Subject: [PATCH 6/7] Update lib/Db/SessionMapper.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: max-nextcloud Signed-off-by: Benjamin Früh <134610227+benjaminfrueh@users.noreply.github.com> --- lib/Db/SessionMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/SessionMapper.php b/lib/Db/SessionMapper.php index c61a97ef8b1..85ad2beb636 100644 --- a/lib/Db/SessionMapper.php +++ b/lib/Db/SessionMapper.php @@ -189,7 +189,7 @@ public function deleteOrphanedSteps(): int { $orphanedStepsQb = $this->db->getQueryBuilder(); $orphanedStepsQb->select('st.id') ->from('text_steps', 'st') - ->leftJoin('st', 'text_sessions', 's', $orphanedStepsQb->expr()->eq('st.session_id', 's.id')) + ->leftJoin('st', 'text_sessions', 's', $orphanedStepsQb->expr()->eq('st.document_id', 's.document_id')) ->leftJoin('st', 'text_documents', 'd', $orphanedStepsQb->expr()->eq('st.document_id', 'd.id')) ->where($orphanedStepsQb->expr()->isNull('s.id')) ->andWhere($orphanedStepsQb->expr()->lt('st.timestamp', $orphanedStepsQb->createNamedParameter($safetyBufferTime))) From e10c24cea5741672a23ab8ed83fd96b830218cd1 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 14 Nov 2025 15:08:14 +0100 Subject: [PATCH 7/7] chore: bring files from branch to latest versions of main branch Signed-off-by: Jonas --- lib/Db/SessionMapper.php | 6 +++--- lib/Service/SessionService.php | 4 ++-- tests/unit/Db/SessionMapperTest.php | 11 ++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/Db/SessionMapper.php b/lib/Db/SessionMapper.php index 85ad2beb636..97e096d5477 100644 --- a/lib/Db/SessionMapper.php +++ b/lib/Db/SessionMapper.php @@ -178,12 +178,12 @@ public function deleteOldSessions(int $ageInSeconds): int { return $deletedCount; } - public function deleteOrphanedSteps(): int { + public function deleteOrphanedSteps(int $ageInSeconds): int { $startTime = microtime(true); $maxExecutionSeconds = 30; $batchSize = 1000; $deletedCount = 0; - $safetyBufferTime = time() - 86400; + $ageThreshold = time() - $ageInSeconds; do { $orphanedStepsQb = $this->db->getQueryBuilder(); @@ -192,7 +192,7 @@ public function deleteOrphanedSteps(): int { ->leftJoin('st', 'text_sessions', 's', $orphanedStepsQb->expr()->eq('st.document_id', 's.document_id')) ->leftJoin('st', 'text_documents', 'd', $orphanedStepsQb->expr()->eq('st.document_id', 'd.id')) ->where($orphanedStepsQb->expr()->isNull('s.id')) - ->andWhere($orphanedStepsQb->expr()->lt('st.timestamp', $orphanedStepsQb->createNamedParameter($safetyBufferTime))) + ->andWhere($orphanedStepsQb->expr()->lt('st.timestamp', $orphanedStepsQb->createNamedParameter($ageThreshold))) ->andWhere( $orphanedStepsQb->expr()->orX( $orphanedStepsQb->expr()->isNull('d.id'), diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index b45648fb4d6..bca114942c9 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -142,8 +142,8 @@ public function removeOldSessions(int $ageInSeconds = 7776000): int { return $this->sessionMapper->deleteOldSessions($ageInSeconds); } - public function removeOrphanedSteps(): int { - return $this->sessionMapper->deleteOrphanedSteps(); + public function removeOrphanedSteps(int $ageInSeconds = 604800): int { + return $this->sessionMapper->deleteOrphanedSteps($ageInSeconds); } public function getSession(int $documentId, int $sessionId, string $token): ?Session { diff --git a/tests/unit/Db/SessionMapperTest.php b/tests/unit/Db/SessionMapperTest.php index ce673bd62ba..eda047c2e52 100644 --- a/tests/unit/Db/SessionMapperTest.php +++ b/tests/unit/Db/SessionMapperTest.php @@ -142,7 +142,7 @@ public function testDeleteOrphanedSteps() { $this->sessionMapper->clearAll(); $this->stepMapper->clearAll(); - $oldTimestamp = time() - 86401; + $eightDaysAgo = time() - (8 * 24 * 60 * 60); // Create document $document = $this->documentMapper->insert(Document::fromParams([ @@ -166,7 +166,7 @@ public function testDeleteOrphanedSteps() { 'sessionId' => 99999, 'documentId' => $document->getId(), 'data' => 'ORPHANED_OLD_VERSION', - 'timestamp' => $oldTimestamp, + 'timestamp' => $eightDaysAgo, 'version' => 1 ])); @@ -185,7 +185,7 @@ public function testDeleteOrphanedSteps() { 'sessionId' => 99999, 'documentId' => $document->getId(), 'data' => 'ORPHANED_NEW_VERSION', - 'timestamp' => $oldTimestamp, + 'timestamp' => $eightDaysAgo, 'version' => 3 ])); @@ -193,8 +193,9 @@ public function testDeleteOrphanedSteps() { self::assertCount(3, $this->stepMapper->find(1, 0)); self::assertCount(1, $this->stepMapper->find(99999, 0)); - // Verify orphan delete - $deletedCount = $this->sessionMapper->deleteOrphanedSteps(); + // Delete orphaned steps older than 7 days + $sevenDays = 7 * 24 * 60 * 60; + $deletedCount = $this->sessionMapper->deleteOrphanedSteps($sevenDays); self::assertEquals(2, $deletedCount); } }