diff --git a/.gitignore b/.gitignore
index 55d9dd391..957307d54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,5 @@ cypress/videos
cypress/snapshots
cypress/downloads
-js/*hot-update.*
\ No newline at end of file
+js/*hot-update.*
+tests/.phpunit.result.cache
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 2e73f5125..1141c153b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -9,7 +9,7 @@
Photos
Your memories under your control
Your memories under your control
- 3.0.1
+ 3.0.2
agpl
John Molakvoæ
Photos
@@ -22,7 +22,6 @@
https://github.com/nextcloud/photos
https://github.com/nextcloud/photos/issues
https://github.com/nextcloud/photos.git
-
diff --git a/composer.json b/composer.json
index cf8cad5b4..fa8ab37f6 100644
--- a/composer.json
+++ b/composer.json
@@ -25,7 +25,7 @@
"psalm:update-baseline": "psalm.phar --threads=1 --update-baseline",
"psalm:clear": "psalm.phar --clear-cache && psalm --clear-global-cache",
"psalm:fix": "psalm.phar --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType",
- "test:unit": "echo 'Only testing installation of the app'"
+ "test:unit": "vendor/bin/phpunit -c tests/phpunit.xml --color --fail-on-warning --fail-on-risky"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
diff --git a/lib/Migration/Version30000Date20240417075404.php b/lib/Migration/Version30000Date20240417075405.php
similarity index 75%
rename from lib/Migration/Version30000Date20240417075404.php
rename to lib/Migration/Version30000Date20240417075405.php
index 2ec038d22..ed3c59b9a 100644
--- a/lib/Migration/Version30000Date20240417075404.php
+++ b/lib/Migration/Version30000Date20240417075405.php
@@ -11,6 +11,7 @@
use Closure;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
@@ -18,7 +19,7 @@
/**
* Migrate the photosSourceFolder user config to photosSourceFolders
*/
-class Version30000Date20240417075404 extends SimpleMigrationStep {
+class Version30000Date20240417075405 extends SimpleMigrationStep {
public function __construct(
private IDBConnection $db,
) {
@@ -36,13 +37,26 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array
->where($select->expr()->eq('appid', $select->expr()->literal('photos')))
->andWhere($select->expr()->eq('configkey', $select->expr()->literal('photosSourceFolders')));
+ $result = $select->executeQuery();
+ $alreadyMigrated = array_map(static fn (array $row) => $row['userid'], $result->fetchAll());
+ $result->closeCursor();
+
// Remove old entries for users who already have the new one
$delete = $this->db->getQueryBuilder();
$delete->delete('preferences')
->where($delete->expr()->eq('appid', $delete->expr()->literal('photos')))
->andWhere($delete->expr()->eq('configkey', $delete->expr()->literal('photosSourceFolder')))
- ->andWhere($delete->expr()->in('userid', $delete->createFunction($select->getSQL())))
- ->executeStatement();
+ ->andWhere($delete->expr()->in(
+ 'userid',
+ $delete->createParameter('chunk'),
+ IQueryBuilder::PARAM_STR
+ ));
+
+ $chunks = array_chunk($alreadyMigrated, 1000);
+ foreach ($chunks as $chunk) {
+ $delete->setParameter('chunk', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
+ $delete->executeStatement();
+ }
// Update remaining old entries to new ones
$update = $this->db->getQueryBuilder();
diff --git a/tests/Album/AlbumMapperTest.php b/tests/Album/AlbumMapperTest.php
index 05dc5515d..3d4d9cd25 100644
--- a/tests/Album/AlbumMapperTest.php
+++ b/tests/Album/AlbumMapperTest.php
@@ -17,6 +17,10 @@
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\IMimeTypeLoader;
use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IUserManager;
+use OCP\Security\ISecureRandom;
use Test\TestCase;
/**
@@ -30,8 +34,16 @@ class AlbumMapperTest extends TestCase {
private $mimeLoader;
/** @var AlbumMapper */
private $mapper;
- /** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var ITimeFactory|MockObject */
private $timeFactory;
+ /** @var IUserManager|MockObject */
+ private $userManager;
+ /** @var IGroupManager|MockObject */
+ private $groupManager;
+ /** @var IL10N|MockObject */
+ private $l10n;
+ /** @var ISecureRandom|MockObject */
+ private $secureRandom;
private int $time = 100;
protected function setUp(): void {
@@ -41,11 +53,27 @@ protected function setUp(): void {
$this->connection = \OC::$server->get(IDBConnection::class);
$this->mimeLoader = \OC::$server->get(IMimeTypeLoader::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->secureRandom = $this->createMock(ISecureRandom::class);
$this->timeFactory->method('getTime')->willReturnCallback(function () {
return $this->time;
});
- $this->mapper = new AlbumMapper($this->connection, $this->mimeLoader, $this->timeFactory);
+ if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
+ $this->markTestSkipped('Feature is broken on oracle');
+ }
+
+ $this->mapper = new AlbumMapper(
+ $this->connection,
+ $this->mimeLoader,
+ $this->timeFactory,
+ $this->userManager,
+ $this->groupManager,
+ $this->l10n,
+ $this->secureRandom,
+ );
}
protected function tearDown():void {
@@ -65,6 +93,7 @@ protected function tearDown():void {
}
private function createFile(string $name, string $mimeType, int $size = 10, int $mtime = 10000, int $permissions = Constants::PERMISSION_ALL): int {
+
$mimeId = $this->mimeLoader->getId($mimeType);
$mimePartId = $this->mimeLoader->getId(substr($mimeType, strpos($mimeType, '/')));
$query = $this->connection->getQueryBuilder();
@@ -152,133 +181,136 @@ public function testCreateUpdateGet() {
$this->assertEquals("nowhere", $retrievedAlbum->getLocation());
}
- public function testEmptyFiles() {
- $album1 = $this->mapper->create("user1", "album1");
-
- $this->assertEquals([new AlbumWithFiles($album1, [])], $this->mapper->getForUserWithFiles("user1"));
- }
-
- public function testAddFiles() {
- $album1 = $this->mapper->create("user1", "album1");
- $album2 = $this->mapper->create("user1", "album2");
-
- $fileId1 = $this->createFile("file1", "text/plain");
- $fileId2 = $this->createFile("file2", "image/png");
-
- $this->mapper->addFile($album1->getId(), $fileId1);
- $this->mapper->addFile($album1->getId(), $fileId2);
- $this->mapper->addFile($album2->getId(), $fileId1);
-
- $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
- usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
- return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
- });
- $this->assertCount(2, $albumsWithFiles);
-
- $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
- $this->assertEquals($fileId2, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
- $files = $albumsWithFiles[0]->getFiles();
- usort($files, function (AlbumFile $a, AlbumFile $b) {
- return $a->getFileId() <=> $b->getFileId();
- });
- $this->assertCount(2, $files);
- $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 100), $albumsWithFiles[0]->getFiles()[0]);
- $this->assertEquals(new AlbumFile($fileId2, "file2", "image/png", 10, 10000, "dummy", 100), $albumsWithFiles[0]->getFiles()[1]);
-
- $this->assertEquals($album2->getId(), $albumsWithFiles[1]->getAlbum()->getId());
- $this->assertEquals($fileId1, $albumsWithFiles[1]->getAlbum()->getLastAddedPhoto());
-
- $files = $albumsWithFiles[1]->getFiles();
- usort($files, function (AlbumFile $a, AlbumFile $b) {
- return $a->getFileId() <=> $b->getFileId();
- });
- $this->assertCount(1, $files);
- $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 100), $albumsWithFiles[0]->getFiles()[0]);
- }
-
- public function testAddRemoveFiles() {
- $album1 = $this->mapper->create("user1", "album1");
-
- $fileId1 = $this->createFile("file1", "text/plain");
- $fileId2 = $this->createFile("file2", "image/png");
- $fileId3 = $this->createFile("file3", "image/png");
-
- $this->time = 110;
- $this->mapper->addFile($album1->getId(), $fileId1);
- $this->time = 120;
- $this->mapper->addFile($album1->getId(), $fileId2);
- $this->time = 130;
- $this->mapper->addFile($album1->getId(), $fileId3);
-
- $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
- usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
- return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
- });
- $this->assertCount(1, $albumsWithFiles);
-
- $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
- $this->assertEquals($fileId3, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
- $files = $albumsWithFiles[0]->getFiles();
- usort($files, function (AlbumFile $a, AlbumFile $b) {
- return $a->getFileId() <=> $b->getFileId();
- });
- $this->assertCount(3, $files);
- $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 110), $albumsWithFiles[0]->getFiles()[0]);
- $this->assertEquals(new AlbumFile($fileId2, "file2", "image/png", 10, 10000, "dummy", 120), $albumsWithFiles[0]->getFiles()[1]);
- $this->assertEquals(new AlbumFile($fileId3, "file3", "image/png", 10, 10000, "dummy", 130), $albumsWithFiles[0]->getFiles()[2]);
-
-
-
- $this->mapper->removeFile($album1->getId(), $fileId2);
-
- $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
- usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
- return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
- });
- $this->assertCount(1, $albumsWithFiles);
-
- $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
- $this->assertEquals($fileId3, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
- $files = $albumsWithFiles[0]->getFiles();
- usort($files, function (AlbumFile $a, AlbumFile $b) {
- return $a->getFileId() <=> $b->getFileId();
- });
- $this->assertCount(2, $files);
- $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 110), $albumsWithFiles[0]->getFiles()[0]);
- $this->assertEquals(new AlbumFile($fileId3, "file3", "image/png", 10, 10000, "dummy", 130), $albumsWithFiles[0]->getFiles()[1]);
-
-
-
- $this->mapper->removeFile($album1->getId(), $fileId3);
-
- $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
- usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
- return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
- });
- $this->assertCount(1, $albumsWithFiles);
-
- $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
- $this->assertEquals($fileId1, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
- $files = $albumsWithFiles[0]->getFiles();
- usort($files, function (AlbumFile $a, AlbumFile $b) {
- return $a->getFileId() <=> $b->getFileId();
- });
- $this->assertCount(1, $files);
- $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 110), $albumsWithFiles[0]->getFiles()[0]);
-
-
-
- $this->mapper->removeFile($album1->getId(), $fileId1);
-
- $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
- usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
- return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
- });
- $this->assertCount(1, $albumsWithFiles);
-
- $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
- $this->assertEquals(-1, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
- $files = $albumsWithFiles[0]->getFiles();
- $this->assertCount(0, $files);
- }
+ /**
+ * Disabled as the function does no longer exist
+ * public function testEmptyFiles() {
+ * $album1 = $this->mapper->create("user1", "album1");
+ *
+ * $this->assertEquals([new AlbumWithFiles($album1, [])], $this->mapper->getForUserWithFiles("user1"));
+ * }
+ *
+ * public function testAddFiles() {
+ * $album1 = $this->mapper->create("user1", "album1");
+ * $album2 = $this->mapper->create("user1", "album2");
+ *
+ * $fileId1 = $this->createFile("file1", "text/plain");
+ * $fileId2 = $this->createFile("file2", "image/png");
+ *
+ * $this->mapper->addFile($album1->getId(), $fileId1, 'user1');
+ * $this->mapper->addFile($album1->getId(), $fileId2, 'user1');
+ * $this->mapper->addFile($album2->getId(), $fileId1, 'user1');
+ *
+ * $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
+ * usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
+ * return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
+ * });
+ * $this->assertCount(2, $albumsWithFiles);
+ *
+ * $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
+ * $this->assertEquals($fileId2, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
+ * $files = $albumsWithFiles[0]->getFiles();
+ * usort($files, function (AlbumFile $a, AlbumFile $b) {
+ * return $a->getFileId() <=> $b->getFileId();
+ * });
+ * $this->assertCount(2, $files);
+ * $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 100, 'user1'), $albumsWithFiles[0]->getFiles()[0]);
+ * $this->assertEquals(new AlbumFile($fileId2, "file2", "image/png", 10, 10000, "dummy", 100, 'user1'), $albumsWithFiles[0]->getFiles()[1]);
+ *
+ * $this->assertEquals($album2->getId(), $albumsWithFiles[1]->getAlbum()->getId());
+ * $this->assertEquals($fileId1, $albumsWithFiles[1]->getAlbum()->getLastAddedPhoto());
+ *
+ * $files = $albumsWithFiles[1]->getFiles();
+ * usort($files, function (AlbumFile $a, AlbumFile $b) {
+ * return $a->getFileId() <=> $b->getFileId();
+ * });
+ * $this->assertCount(1, $files);
+ * $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 100, 'user1'), $albumsWithFiles[0]->getFiles()[0]);
+ * }
+ *
+ * public function testAddRemoveFiles() {
+ * $album1 = $this->mapper->create("user1", "album1");
+ *
+ * $fileId1 = $this->createFile("file1", "text/plain");
+ * $fileId2 = $this->createFile("file2", "image/png");
+ * $fileId3 = $this->createFile("file3", "image/png");
+ *
+ * $this->time = 110;
+ * $this->mapper->addFile($album1->getId(), $fileId1, 'user1');
+ * $this->time = 120;
+ * $this->mapper->addFile($album1->getId(), $fileId2, 'user1');
+ * $this->time = 130;
+ * $this->mapper->addFile($album1->getId(), $fileId3, 'user1');
+ *
+ * $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
+ * usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
+ * return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
+ * });
+ * $this->assertCount(1, $albumsWithFiles);
+ *
+ * $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
+ * $this->assertEquals($fileId3, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
+ * $files = $albumsWithFiles[0]->getFiles();
+ * usort($files, function (AlbumFile $a, AlbumFile $b) {
+ * return $a->getFileId() <=> $b->getFileId();
+ * });
+ * $this->assertCount(3, $files);
+ * $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 110, 'user1'), $albumsWithFiles[0]->getFiles()[0]);
+ * $this->assertEquals(new AlbumFile($fileId2, "file2", "image/png", 10, 10000, "dummy", 120, 'user1'), $albumsWithFiles[0]->getFiles()[1]);
+ * $this->assertEquals(new AlbumFile($fileId3, "file3", "image/png", 10, 10000, "dummy", 130, 'user1'), $albumsWithFiles[0]->getFiles()[2]);
+ *
+ *
+ *
+ * $this->mapper->removeFile($album1->getId(), $fileId2);
+ *
+ * $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
+ * usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
+ * return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
+ * });
+ * $this->assertCount(1, $albumsWithFiles);
+ *
+ * $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
+ * $this->assertEquals($fileId3, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
+ * $files = $albumsWithFiles[0]->getFiles();
+ * usort($files, function (AlbumFile $a, AlbumFile $b) {
+ * return $a->getFileId() <=> $b->getFileId();
+ * });
+ * $this->assertCount(2, $files);
+ * $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 110, 'user1'), $albumsWithFiles[0]->getFiles()[0]);
+ * $this->assertEquals(new AlbumFile($fileId3, "file3", "image/png", 10, 10000, "dummy", 130, 'user1'), $albumsWithFiles[0]->getFiles()[1]);
+ *
+ *
+ *
+ * $this->mapper->removeFile($album1->getId(), $fileId3);
+ *
+ * $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
+ * usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
+ * return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
+ * });
+ * $this->assertCount(1, $albumsWithFiles);
+ *
+ * $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
+ * $this->assertEquals($fileId1, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
+ * $files = $albumsWithFiles[0]->getFiles();
+ * usort($files, function (AlbumFile $a, AlbumFile $b) {
+ * return $a->getFileId() <=> $b->getFileId();
+ * });
+ * $this->assertCount(1, $files);
+ * $this->assertEquals(new AlbumFile($fileId1, "file1", "text/plain", 10, 10000, "dummy", 110, 'user1'), $albumsWithFiles[0]->getFiles()[0]);
+ *
+ *
+ *
+ * $this->mapper->removeFile($album1->getId(), $fileId1);
+ *
+ * $albumsWithFiles = $this->mapper->getForUserWithFiles("user1");
+ * usort($albumsWithFiles, function (AlbumWithFiles $a, AlbumWithFiles $b) {
+ * return $a->getAlbum()->getId() <=> $b->getAlbum()->getId();
+ * });
+ * $this->assertCount(1, $albumsWithFiles);
+ *
+ * $this->assertEquals($album1->getId(), $albumsWithFiles[0]->getAlbum()->getId());
+ * $this->assertEquals(-1, $albumsWithFiles[0]->getAlbum()->getLastAddedPhoto());
+ * $files = $albumsWithFiles[0]->getFiles();
+ * $this->assertCount(0, $files);
+ * }
+ */
}
diff --git a/tests/Migration/Version30000Date20240417075405Test.php b/tests/Migration/Version30000Date20240417075405Test.php
new file mode 100644
index 000000000..4d0d73055
--- /dev/null
+++ b/tests/Migration/Version30000Date20240417075405Test.php
@@ -0,0 +1,102 @@
+connection = \OCP\Server::get(IDBConnection::class);
+ }
+
+ protected function tearDown(): void {
+ $this->deleteTestEntries();
+ parent::tearDown();
+ }
+
+ protected function deleteTestEntries(): void {
+ $delete = $this->connection->getQueryBuilder();
+ $delete->delete('preferences')
+ ->where($delete->expr()->like('userid', $delete->createNamedParameter('Version30000Date20240417075405Test%')));
+ $delete->executeStatement();
+ }
+
+ protected function createTestEntries(): void {
+ $this->deleteTestEntries();
+
+ $insert = $this->connection->getQueryBuilder();
+ $insert->insert('preferences')
+ ->values([
+ 'userid' => $insert->createParameter('userid'),
+ 'appid' => $insert->createNamedParameter('photos'),
+ 'configkey' => $insert->createParameter('configkey'),
+ 'configvalue' => $insert->createNamedParameter('value'),
+ ]);
+
+ $this->connection->beginTransaction();
+ for ($i = 1; $i <= 3000; $i++) {
+ $mod = $i % 3;
+ if ($mod !== 0) {
+ $insert->setParameter('userid', 'Version30000Date20240417075405Test#' . $i)
+ ->setParameter('configkey', 'photosSourceFolder');
+ $insert->executeStatement();
+ }
+ if ($mod !== 1) {
+ $insert->setParameter('userid', 'Version30000Date20240417075405Test#' . $i)
+ ->setParameter('configkey', 'photosSourceFolders');
+ $insert->executeStatement();
+ }
+ }
+ $this->connection->commit();
+ }
+
+ public function testPostSchemaChange(): void {
+ $migration = new Version30000Date20240417075405($this->connection);
+
+ $this->createTestEntries();
+
+
+ $migration->postSchemaChange(
+ $this->createMock(IOutput::class),
+ \Closure::fromCallable(fn () => false),
+ []
+ );
+
+ $select = $this->connection->getQueryBuilder();
+ $select->select($select->func()->count())
+ ->from('preferences')
+ ->where($select->expr()->like('userid', $select->createNamedParameter('Version30000Date20240417075405Test%')))
+ ->andWhere($select->expr()->eq('appid', $select->expr()->literal('photos')))
+ ->andWhere($select->expr()->eq('configkey', $select->expr()->literal('photosSourceFolder')));
+ $result = $select->executeQuery();
+ $this->assertEquals(0, $result->fetchOne());
+ $result->closeCursor();
+
+ $select = $this->connection->getQueryBuilder();
+ $select->select($select->func()->count())
+ ->from('preferences')
+ ->where($select->expr()->like('userid', $select->createNamedParameter('Version30000Date20240417075405Test%')))
+ ->andWhere($select->expr()->eq('appid', $select->expr()->literal('photos')))
+ ->andWhere($select->expr()->eq('configkey', $select->expr()->literal('photosSourceFolders')));
+ $result = $select->executeQuery();
+ $this->assertEquals(3000, $result->fetchOne());
+ $result->closeCursor();
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 000000000..b3f435f93
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,23 @@
+addValidRoot(OC::$SERVERROOT . '/tests');
+
+// Fix for "Autoload path not allowed: .../photos/tests/testcase.php"
+\OC_App::loadApp('photos');
+
+if (!class_exists('\PHPUnit\Framework\TestCase')) {
+ require_once('PHPUnit/Autoload.php');
+}
+
+OC_Hook::clear();
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
new file mode 100644
index 000000000..3e7c2ec9c
--- /dev/null
+++ b/tests/phpunit.xml
@@ -0,0 +1,13 @@
+
+
+
+ .
+
+
+