[BUGFIX] Prevent orphaned tags in Typo3DatabaseBackend 09/49309/10
authorThomas Schlumberger <thomas@b13.de>
Tue, 2 Aug 2016 12:29:58 +0000 (14:29 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Mon, 12 Sep 2016 23:38:23 +0000 (01:38 +0200)
The 7.6 and 6.2 implementation of Typo3DatabaseBackend cache backend has
bugs in two methods (in mysql-non-dbal versions). Those were introduced
by #61814 and fixed in master with #77160.

flushByTag() leaves orphaned tags in tags table - if a row has two tags
and flushByTag() is executed on one tag, the other is left.

collectGargabe() does not find orphaned tags collectGarbage() and does
not delete an expired cache row if it has no tags (fix: left outer join)

The patch migrates the functional tests and fixes the issue.

Change-Id: Ie53f54eceb3e47c21c31e7263a3f855b1cb93660
Resolves: #77204
Releases: 7.6, 6.2
Reviewed-on: https://review.typo3.org/49309
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php
typo3/sysext/core/Tests/Functional/Cache/Backend/Typo3DatabaseBackendTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php

index 806fe35..2410d94 100644 (file)
@@ -13,12 +13,15 @@ namespace TYPO3\CMS\Core\Cache\Backend;
  *
  * The TYPO3 project - inspiring people to share!
  */
+use TYPO3\CMS\Core\Cache\Exception;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 
 /**
  * A caching backend which stores cache entries in database tables
  * @api
  */
-class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
+class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInterface
 {
     /**
      * @var int Timestamp of 2038-01-01)
@@ -86,7 +89,7 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
      * @return void
      * @api
      */
-    public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
+    public function setCache(FrontendInterface $cache)
     {
         parent::setCache($cache);
         $this->cacheTable = 'cf_' . $this->cacheIdentifier;
@@ -125,7 +128,7 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     {
         $this->throwExceptionIfFrontendDoesNotExist();
         if (!is_string($data)) {
-            throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException(
+            throw new Exception\InvalidDataException(
                 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
                 1236518298
             );
@@ -217,15 +220,18 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     {
         $this->throwExceptionIfFrontendDoesNotExist();
         $entryRemoved = false;
-        $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
+        $GLOBALS['TYPO3_DB']->exec_DELETEquery(
             $this->cacheTable,
             'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable)
         );
+        // we need to save the affected rows as mysqli_affected_rows just returns the amount of affected rows
+        // of the last call
+        $affectedRows = $GLOBALS['TYPO3_DB']->sql_affected_rows();
         $GLOBALS['TYPO3_DB']->exec_DELETEquery(
             $this->tagsTable,
             'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->tagsTable)
         );
-        if ($GLOBALS['TYPO3_DB']->sql_affected_rows($res) == 1) {
+        if ($affectedRows == 1) {
             $entryRemoved = true;
         }
         return $entryRemoved;
@@ -275,36 +281,27 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     {
         $this->throwExceptionIfFrontendDoesNotExist();
 
-        if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('dbal')) {
-            $this->flushByTagDbal($tag);
-        } else {
+        if ($this->isConnectionMysql()) {
             $GLOBALS['TYPO3_DB']->sql_query('
-                               DELETE ' . $this->cacheTable . ', ' . $this->tagsTable . '
-                               FROM ' . $this->cacheTable . ' JOIN ' . $this->tagsTable . ' ON ' . $this->cacheTable . '.identifier=' . $this->tagsTable . '.identifier
-                               WHERE ' . $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable)
+                DELETE tags2, cache1'
+                . ' FROM ' . $this->tagsTable . ' AS tags1'
+                . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
+                . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
+                . ' WHERE tags1.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable)
             );
-        }
-    }
-
-    /**
-     * Removes all cache entries of this cache for DBAL databases which are tagged by the specified tag.
-     *
-     * @param string $tag The tag the entries must have
-     * @return void
-     */
-    protected function flushByTagDbal($tag)
-    {
-        $tagsTableWhereClause = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
-        $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->tagsTable, $tagsTableWhereClause);
-        $cacheEntryIdentifiers = [];
-        while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
-            $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->cacheTable);
-        }
-        $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
-        if (!empty($cacheEntryIdentifiers)) {
-            $deleteWhereClause = 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')';
-            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, $deleteWhereClause);
-            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, $deleteWhereClause);
+        } else {
+            $tagsTableWhereClause = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
+            $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->tagsTable, $tagsTableWhereClause);
+            $cacheEntryIdentifiers = [];
+            while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
+                $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->cacheTable);
+            }
+            $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
+            if (!empty($cacheEntryIdentifiers)) {
+                $deleteWhereClause = 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')';
+                $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, $deleteWhereClause);
+                $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, $deleteWhereClause);
+            }
         }
     }
 
@@ -317,37 +314,57 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     {
         $this->throwExceptionIfFrontendDoesNotExist();
 
-        if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('dbal')) {
-            $this->collectGarbageDbal();
+        if ($this->isConnectionMysql()) {
+            // First delete all expired rows from cache table and their connected tag rows
+            $GLOBALS['TYPO3_DB']->sql_query(
+                'DELETE cache, tags'
+                . ' FROM ' . $this->cacheTable . ' AS cache'
+                . ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
+                . ' WHERE cache.expires < ' . $GLOBALS['EXEC_TIME']
+            );
+            // Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
+            $GLOBALS['TYPO3_DB']->sql_query(
+                'DELETE tags'
+                . ' FROM ' . $this->tagsTable . ' AS tags'
+                . ' LEFT OUTER JOIN ' . $this->cacheTable . ' AS cache ON tags.identifier = cache.identifier'
+                . ' WHERE cache.identifier IS NULL'
+            );
         } else {
-            $GLOBALS['TYPO3_DB']->sql_query('
-                               DELETE ' . $this->cacheTable . ', ' . $this->tagsTable . '
-                               FROM ' . $this->cacheTable . ' JOIN ' . $this->tagsTable . ' ON ' . $this->cacheTable . '.identifier=' . $this->tagsTable . '.identifier
-                               WHERE ' . $this->expiredStatement
+            // Get identifiers of expired cache entries
+            $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->cacheTable, 'expires < ' . $GLOBALS['EXEC_TIME']);
+            $cacheEntryIdentifiers = [];
+            while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
+                $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->tagsTable);
+            }
+            $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
+            // Delete tag rows connected to expired cache entries
+            if (!empty($cacheEntryIdentifiers)) {
+                $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')');
+            }
+            // Delete expired cache rows
+            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, 'expires < ' . $GLOBALS['EXEC_TIME']);
+
+            // Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
+            $result = $GLOBALS['TYPO3_DB']->sql_query(
+                'SELECT tags.identifier'
+                . ' FROM ' . $this->tagsTable . ' AS tags'
+                . ' LEFT OUTER JOIN ' . $this->cacheTable . ' AS cache ON tags.identifier = cache.identifier'
+                . ' WHERE cache.identifier IS NULL'
+                . ' GROUP BY tags.identifier'
             );
-        }
-    }
 
-    /**
-     * Does garbage collection for DBAL databases
-     *
-     * @return void
-     */
-    protected function collectGarbageDbal()
-    {
-        // Get identifiers of expired cache entries
-        $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->cacheTable, $this->expiredStatement);
-        $cacheEntryIdentifiers = [];
-        while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
-            $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->tagsTable);
-        }
-        $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
-        // Delete tag rows connected to expired cache entries
-        if (!empty($cacheEntryIdentifiers)) {
-            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')');
+            while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result)) {
+                $tagsEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($row['identifier'], $this->tagsTable);
+            }
+
+            if (!empty($tagsEntryIdentifiers)) {
+                $GLOBALS['TYPO3_DB']->sql_query(
+                    'DELETE'
+                    . ' FROM ' . $this->tagsTable
+                    . ' WHERE identifier IN (' . implode(',', $tagsEntryIdentifiers) . ')'
+                );
+            }
         }
-        // Delete expired cache rows
-        $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, $this->expiredStatement);
     }
 
     /**
@@ -404,8 +421,8 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
      */
     protected function throwExceptionIfFrontendDoesNotExist()
     {
-        if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
-            throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set via setCache() yet.', 1236518288);
+        if (!$this->cache instanceof FrontendInterface) {
+            throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
         }
     }
 
@@ -419,15 +436,26 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     public function getTableDefinitions()
     {
         $cacheTableSql = file_get_contents(
-            \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
+            ExtensionManagementUtility::extPath('core') .
             'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
         );
         $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
         $tagsTableSql = file_get_contents(
-            \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
+            ExtensionManagementUtility::extPath('core') .
             'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
         );
         $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
         return $requiredTableStructures;
     }
+
+    /**
+     * This database backend uses some optimized queries for mysql
+     * to get maximum performance.
+     *
+     * @return bool
+     */
+    protected function isConnectionMysql()
+    {
+        return !((bool)ExtensionManagementUtility::isLoaded('dbal'));
+    }
 }
diff --git a/typo3/sysext/core/Tests/Functional/Cache/Backend/Typo3DatabaseBackendTest.php b/typo3/sysext/core/Tests/Functional/Cache/Backend/Typo3DatabaseBackendTest.php
new file mode 100644 (file)
index 0000000..21318c1
--- /dev/null
@@ -0,0 +1,732 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Functional\Cache\Backend;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Tests\FunctionalTestCase;
+
+/**
+ * Test case
+ */
+class Typo3DatabaseBackendTest extends FunctionalTestCase
+{
+
+    /**
+     * @test
+     */
+    public function getReturnsPreviouslySetEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myData');
+        $this->assertSame('myData', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsPreviouslySetEntryWithNewContentIfSetWasCalledMultipleTimes()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myData');
+        $subject->set('myIdentifier', 'myNewData');
+        $this->assertSame('myNewData', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function setInsertsDataWithTagsIntoCacheTable()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myData', ['aTag', 'anotherTag']);
+
+        $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="myIdentifier"');
+        $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="myIdentifier" AND tag="aTag"');
+        $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="myIdentifier" AND tag="anotherTag"');
+    }
+
+    /**
+     * @test
+     */
+    public function setStoresCompressedContent()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Have backend with compression enabled
+        $subject = new Typo3DatabaseBackend('Testing', ['compression' => true]);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myCachedContent');
+
+        $row = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
+            'content',
+            'cf_cache_pages',
+            'identifier="myIdentifier"'
+        );
+
+        // Content comes back uncompressed
+        $this->assertSame('myCachedContent', gzuncompress($row['content']));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsFalseIfNoCacheEntryExists()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsFalseForExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push an expired row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] - 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsNotExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertSame('myCachedContent', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsUnzipsNotExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a compressed row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => gzcompress('myCachedContent'),
+            ]
+        );
+
+
+
+        // Have backend with compression enabled
+        $subject = new Typo3DatabaseBackend('Testing', ['compression' => true]);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // Content comes back uncompressed
+        $this->assertSame('myCachedContent', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsEmptyStringUnzipped()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a compressed row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => gzcompress(''),
+            ]
+        );
+
+        // Have backend with compression enabled
+        $subject = new Typo3DatabaseBackend('Testing', ['compression' => true]);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // Content comes back uncompressed
+        $this->assertSame('', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function hasReturnsFalseIfNoCacheEntryExists()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->has('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function hasReturnsFalseForExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push an expired row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] - 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->has('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function hasReturnsNotExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertTrue($subject->has('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function removeReturnsFalseIfNoEntryHasBeenRemoved()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->remove('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function removeReturnsTrueIfAnEntryHasBeenRemoved()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a row into db
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+        $this->assertTrue($subject->remove('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function removeRemovesCorrectEntriesFromDatabase()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Add one cache row to remove and another one that shouldn't be removed
+        $GLOBALS['TYPO3_DB']->INSERTmultipleRows(
+            'cf_cache_pages',
+            ['identifier', 'expires', 'content'],
+            [
+                ['myIdentifier', $GLOBALS['EXEC_TIME'] + 60, 'myCachedContent'],
+                ['otherIdentifier', $GLOBALS['EXEC_TIME'] + 60, 'otherCachedContent'],
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        // Add a couple of tags
+        $GLOBALS['TYPO3_DB']->INSERTmultipleRows(
+            'cf_cache_pages',
+            ['identifier', 'tag'],
+            [
+                ['myIdentifier', 'aTag'],
+                ['myIdentifier', 'otherTag'],
+                ['otherIdentifier', 'aTag'],
+                ['otherIdentifier', 'otherTag'],
+            ]
+        );
+
+        $subject->remove('myIdentifier');
+
+        // cache row with removed identifier has been removed, other one exists
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="myIdentifier"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="otherIdentifier"'));
+
+        // tags of myIdentifier should have been removed, others exist
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="myIdentifier"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="otherIdentifier"'));
+    }
+
+    /**
+     * @test
+     */
+    public function findIdentifiersByTagReturnsIdentifierTaggedWithGivenTag()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC']);
+
+        $this->assertSame(['idA' => 'idA'], $subject->findIdentifiersByTag('tagA'));
+        $this->assertSame(['idA' => 'idA', 'idB' => 'idB'], $subject->findIdentifiersByTag('tagB'));
+        $this->assertSame(['idB' => 'idB'], $subject->findIdentifiersByTag('tagC'));
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagWorksWithEmptyCacheTablesWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->flushByTag('tagB');
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagRemovesCorrectRowsFromDatabaseWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC']);
+        $subject->set('idC', 'dataC', ['tagC', 'tagD']);
+        $subject->flushByTag('tagB');
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idA"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idB"'));
+        $this->assertSame(1, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idC"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idA"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idB"'));
+        $this->assertSame(2, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idC"'));
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagWorksWithEmptyCacheTablesWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->flushByTag('tagB');
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagRemovesCorrectRowsFromDatabaseWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC']);
+        $subject->set('idC', 'dataC', ['tagC', 'tagD']);
+        $subject->flushByTag('tagB');
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idA"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idB"'));
+        $this->assertSame(1, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idC"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idA"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idB"'));
+        $this->assertSame(2, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idC"'));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageWorksWithEmptyTableWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->collectGarbage();
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesCacheEntryWithExpiredLifetimeWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // idA should be expired after EXEC_TIME manipulation, idB should stay
+        $subject->set('idA', 'dataA', [], 60);
+        $subject->set('idB', 'dataB', [], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idA"'));
+        $this->assertSame(1, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idB"'));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesTagEntriesForCacheEntriesWithExpiredLifetimeWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idA"'));
+        $this->assertSame(2, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idB"'));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesOrphanedTagEntriesFromTagsTableWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        // Push two orphaned tag row into db - tags that have no related cache record anymore for whatever reason
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagC'
+            ]
+        );
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagD'
+            ]
+        );
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idA"'));
+        $this->assertSame(2, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idB"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idC"'));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageWorksWithEmptyTableWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->collectGarbage();
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesCacheEntryWithExpiredLifetimeWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // idA should be expired after EXEC_TIME manipulation, idB should stay
+        $subject->set('idA', 'dataA', [], 60);
+        $subject->set('idB', 'dataB', [], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idA"'));
+        $this->assertSame(1, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages', 'identifier="idB"'));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesTagEntriesForCacheEntriesWithExpiredLifetimeWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idA"'));
+        $this->assertSame(2, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idB"'));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesOrphanedTagEntriesFromTagsTableWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        // Push two orphaned tag row into db - tags that have no related cache record anymore for whatever reason
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagC'
+            ]
+        );
+        $GLOBALS['TYPO3_DB']->exec_INSERTquery(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagD'
+            ]
+        );
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idA"'));
+        $this->assertSame(2, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idB"'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags', 'identifier="idC"'));
+    }
+
+    /**
+     * @test
+     */
+    public function flushLeavesCacheAndTagsTableEmpty()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+
+        $subject->flush();
+
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages'));
+        $this->assertSame(0, $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cf_cache_pages_tags'));
+    }
+}
index 6128636..f949719 100644 (file)
@@ -332,36 +332,6 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
 
     /**
      * @test
-     */
-    public function removeReallyRemovesACacheEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMock(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class, ['dummy'], ['Testing']);
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->getMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class, [], [], '', false);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(0))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('aIdentifier'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing', 'identifier = aIdentifier');
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(2))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('aIdentifier'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(3))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing_tags', 'identifier = aIdentifier');
-
-        $backend->remove('aIdentifier');
-    }
-
-    /**
-     * @test
      * @expectedException \TYPO3\CMS\Core\Cache\Exception
      */
     public function collectGarbageThrowsExceptionIfFrontendWasNotSet()
@@ -403,28 +373,6 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
 
     /**
      * @test
-     */
-    public function collectGarbageDeletesExpiredEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMock(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class, ['dummy'], ['Testing']);
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->getMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class, [], [], '', false);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(false));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(3))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing', $this->stringContains('cf_Testing.expires < '));
-
-        $backend->collectGarbage();
-    }
-
-    /**
-     * @test
      * @expectedException \TYPO3\CMS\Core\Cache\Exception
      */
     public function findIdentifiersByTagThrowsExceptionIfFrontendWasNotSet()