diff --git a/.github/workflows/npm-audit-fix.yml b/.github/workflows/npm-audit-fix.yml index 5cf705b27..2074c4c38 100644 --- a/.github/workflows/npm-audit-fix.yml +++ b/.github/workflows/npm-audit-fix.yml @@ -67,22 +67,6 @@ jobs: npm ci npm run build --if-present - - name: Generate PR body - if: steps.checkout.outcome == 'success' - run: | - { - printf '%s\n\n' "$NPM_AUDIT_MARKDOWN" - echo '## Full `npm audit` report' - echo '' - echo '```' - npm audit 2>&1 || true - echo '```' - echo '' - echo "**Node.js:** $(node --version) | **npm:** $(npm --version) | **Branch:** ${{ matrix.branches }}" - } > pr-body.md - env: - NPM_AUDIT_MARKDOWN: ${{ steps.npm-audit.outputs.markdown }} - - name: Create Pull Request if: steps.checkout.outcome == 'success' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 @@ -94,7 +78,7 @@ jobs: signoff: true branch: automated/noid/${{ matrix.branches }}-fix-npm-audit title: '[${{ matrix.branches }}] Fix npm audit' - body-path: pr-body.md + body: ${{ steps.npm-audit.outputs.markdown }} labels: | dependencies 3. to review diff --git a/composer.lock b/composer.lock index 025d80afb..bff82a9f1 100644 --- a/composer.lock +++ b/composer.lock @@ -70,12 +70,12 @@ "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "f3fddf2af92f48d3f53569074f69f0ae4bad52b6" + "reference": "69ce9a120906944a58b3953b29c56b809eefc9bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/f3fddf2af92f48d3f53569074f69f0ae4bad52b6", - "reference": "f3fddf2af92f48d3f53569074f69f0ae4bad52b6", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/69ce9a120906944a58b3953b29c56b809eefc9bd", + "reference": "69ce9a120906944a58b3953b29c56b809eefc9bd", "shasum": "" }, "require": { @@ -112,7 +112,7 @@ "issues": "https://github.com/nextcloud-deps/ocp/issues", "source": "https://github.com/nextcloud-deps/ocp/tree/master" }, - "time": "2026-05-09T01:52:54+00:00" + "time": "2026-04-30T01:55:09+00:00" }, { "name": "psr/clock", diff --git a/l10n/it.js b/l10n/it.js index 3337a2222..bead57d54 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -51,7 +51,6 @@ OC.L10N.register( "Loading activities" : "Caricamento delle attività", "This stream will show events like additions, changes & shares" : "Questo flusso mostrerà gli eventi come aggiunte, cambiamenti e condivisioni", "No activity yet" : "Ancora nessuna attività", - "New activities" : "Nuove attività", "Loading more activities" : "Caricamento di altre attività", "No more activities." : "Nessun'altra attività.", "Could not enable RSS link" : "Impossibile attivare il collegamento RSS", diff --git a/l10n/it.json b/l10n/it.json index a4d5dd9b5..ea72a1d24 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -49,7 +49,6 @@ "Loading activities" : "Caricamento delle attività", "This stream will show events like additions, changes & shares" : "Questo flusso mostrerà gli eventi come aggiunte, cambiamenti e condivisioni", "No activity yet" : "Ancora nessuna attività", - "New activities" : "Nuove attività", "Loading more activities" : "Caricamento di altre attività", "No more activities." : "Nessun'altra attività.", "Could not enable RSS link" : "Impossibile attivare il collegamento RSS", diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 19c4f47ef..312aab673 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -31,6 +31,7 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\DB\Events\AddMissingIndicesEvent; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeFormatter; use OCP\IDBConnection; @@ -117,6 +118,7 @@ public function register(IRegistrationContext $context): void { $c->get(IFactory::class), $c->get(IManager::class), $c->get(IValidator::class), + $c->get(IAppConfig::class), $c->get(IConfig::class), $c->get(LoggerInterface::class), $c->get(Data::class), diff --git a/lib/Controller/APIv2Controller.php b/lib/Controller/APIv2Controller.php index e0444a0c3..2270bef69 100644 --- a/lib/Controller/APIv2Controller.php +++ b/lib/Controller/APIv2Controller.php @@ -28,14 +28,29 @@ use OCP\Notification\IManager as INotificationManager; class APIv2Controller extends OCSController { - protected string $filter = 'all'; - protected int $since = 0; - protected int $limit = 50; - protected string $sort = 'desc'; - protected string $objectType = ''; - protected int $objectId = 0; - protected string $user = ''; - protected bool $loadPreviews = false; + /** @var string */ + protected $filter; + + /** @var int */ + protected $since; + + /** @var int */ + protected $limit; + + /** @var string */ + protected $sort; + + /** @var string */ + protected $objectType; + + /** @var int */ + protected $objectId; + + /** @var string */ + protected $user; + + /** @var bool */ + protected $loadPreviews; public function __construct( $appName, @@ -56,19 +71,26 @@ public function __construct( } /** + * @param string $filter + * @param int $since + * @param int $limit + * @param bool $previews + * @param string $objectType + * @param int $objectId + * @param string $sort * @throws InvalidFilterException when the filter is invalid * @throws \OutOfBoundsException when no user is given */ - protected function validateParameters(string $filter, int $since, int $limit, bool $previews, string $objectType, int $objectId, string $sort): void { - $this->filter = $filter; + protected function validateParameters($filter, $since, $limit, $previews, $objectType, $objectId, $sort) { + $this->filter = \is_string($filter) ? $filter : 'all'; if ($this->filter !== $this->data->validateFilter($this->filter)) { throw new InvalidFilterException('Invalid filter'); } - $this->since = $since; - $this->limit = $limit; - $this->loadPreviews = $previews; - $this->objectType = $objectType; - $this->objectId = $objectId; + $this->since = (int)$since; + $this->limit = (int)$limit; + $this->loadPreviews = (bool)$previews; + $this->objectType = (string)$objectType; + $this->objectId = (int)$objectId; $this->sort = \in_array($sort, ['asc', 'desc'], true) ? $sort : 'desc'; if (($this->objectType !== '' && $this->objectId === 0) || ($this->objectType === '' && $this->objectId !== 0)) { @@ -88,15 +110,32 @@ protected function validateParameters(string $filter, int $since, int $limit, bo /** * @NoAdminRequired + * + * @param int $since + * @param int $limit + * @param bool $previews + * @param string $object_type + * @param int $object_id + * @param string $sort + * @return DataResponse */ - public function getDefault(int $since = 0, int $limit = 50, bool $previews = false, string $object_type = '', int $object_id = 0, string $sort = 'desc'): DataResponse { + public function getDefault($since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc'): DataResponse { return $this->get('all', $since, $limit, $previews, $object_type, $object_id, $sort); } /** * @NoAdminRequired + * + * @param string $filter + * @param int $since + * @param int $limit + * @param bool $previews + * @param string $object_type + * @param int $object_id + * @param string $sort + * @return DataResponse */ - public function getFilter(string $filter, int $since = 0, int $limit = 50, bool $previews = false, string $object_type = '', int $object_id = 0, string $sort = 'desc'): DataResponse { + public function getFilter($filter, $since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc'): DataResponse { return $this->get($filter, $since, $limit, $previews, $object_type, $object_id, $sort); } @@ -152,7 +191,17 @@ public function listFilters(): DataResponse { return new DataResponse($filters); } - protected function get(string $filter, int $since, int $limit, bool $previews, string $filterObjectType, int $filterObjectId, string $sort): DataResponse { + /** + * @param string $filter + * @param int $since + * @param int $limit + * @param bool $previews + * @param string $filterObjectType + * @param int $filterObjectId + * @param string $sort + * @return DataResponse + */ + protected function get($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort): DataResponse { try { $this->validateParameters($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort); } catch (InvalidFilterException $e) { diff --git a/lib/Data.php b/lib/Data.php index 06e7867af..b4d444bdf 100644 --- a/lib/Data.php +++ b/lib/Data.php @@ -226,7 +226,7 @@ public function storeMail(IEvent $event, int $latestSendTime): bool { * @return array * */ - public function get(GroupHelper $groupHelper, UserSettings $userSettings, string $user, int $since, int $limit, string $sort, string $filter, string $objectType = '', int $objectId = 0, bool $returnEvents = false): array { + public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, $since, $limit, $sort, $filter, $objectType = '', $objectId = 0, bool $returnEvents = false) { // get current user if ($user === '') { throw new \OutOfBoundsException('Invalid user', 1); @@ -323,39 +323,36 @@ public function get(GroupHelper $groupHelper, UserSettings $userSettings, string * * @throws \OutOfBoundsException If $since is not owned by $user */ - protected function setOffsetFromSince(IQueryBuilder $query, string $user, int $since, string $sort): array { - if (!$since) { - return $this->getFirstKnownActivityHeader($user, $sort); - } - - $queryBuilder = $this->connection->getQueryBuilder(); - $queryBuilder->select(['affecteduser', 'timestamp']) - ->from('activity') - ->where($queryBuilder->expr()->eq('activity_id', $queryBuilder->createNamedParameter($since))); - $result = $queryBuilder->executeQuery(); - $activity = $result->fetch(); - $result->closeCursor(); - - if (!$activity) { - return $this->getFirstKnownActivityHeader($user, $sort); - } - - if ($activity['affecteduser'] !== $user) { - throw new \OutOfBoundsException('Invalid since', 2); - } - - $timestamp = (int)$activity['timestamp']; - if ($sort === 'DESC') { - $query->andWhere($query->expr()->lte('timestamp', $query->createNamedParameter($timestamp))); - $query->andWhere($query->expr()->lt('activity_id', $query->createNamedParameter($since))); - } else { - $query->andWhere($query->expr()->gte('timestamp', $query->createNamedParameter($timestamp))); - $query->andWhere($query->expr()->gt('activity_id', $query->createNamedParameter($since))); + protected function setOffsetFromSince(IQueryBuilder $query, $user, $since, $sort) { + if ($since) { + $queryBuilder = $this->connection->getQueryBuilder(); + $queryBuilder->select(['affecteduser', 'timestamp']) + ->from('activity') + ->where($queryBuilder->expr()->eq('activity_id', $queryBuilder->createNamedParameter((int)$since))); + $result = $queryBuilder->executeQuery(); + $activity = $result->fetch(); + $result->closeCursor(); + + if ($activity) { + if ($activity['affecteduser'] !== $user) { + throw new \OutOfBoundsException('Invalid since', 2); + } + $timestamp = (int)$activity['timestamp']; + + if ($sort === 'DESC') { + $query->andWhere($query->expr()->lte('timestamp', $query->createNamedParameter($timestamp))); + $query->andWhere($query->expr()->lt('activity_id', $query->createNamedParameter($since))); + } else { + $query->andWhere($query->expr()->gte('timestamp', $query->createNamedParameter($timestamp))); + $query->andWhere($query->expr()->gt('activity_id', $query->createNamedParameter($since))); + } + return []; + } } - return []; - } - private function getFirstKnownActivityHeader(string $user, string $sort): array { + /** + * Couldn't find the since, so find the oldest one and set the header + */ $fetchQuery = $this->connection->getQueryBuilder(); $fetchQuery->select('activity_id') ->from('activity') @@ -367,8 +364,11 @@ private function getFirstKnownActivityHeader(string $user, string $sort): array $result->closeCursor(); if ($activity !== false) { - return ['X-Activity-First-Known' => (int)$activity['activity_id']]; + return [ + 'X-Activity-First-Known' => (int)$activity['activity_id'], + ]; } + return []; } diff --git a/lib/FilesHooks.php b/lib/FilesHooks.php index e401f9fde..dbcb85ae5 100644 --- a/lib/FilesHooks.php +++ b/lib/FilesHooks.php @@ -82,10 +82,10 @@ public function fileCreate($path) { return; } - if ($this->currentUser->getUserIdentifier() === '' && $this->currentUser->isPublicShareToken()) { - $this->addNotificationsForFileAction($path, Files_Sharing::TYPE_PUBLIC_UPLOAD, '', 'created_public'); - } else { + if ($this->currentUser->getUserIdentifier() !== '' || !$this->currentUser->isPublicShareToken()) { $this->addNotificationsForFileAction($path, Files::TYPE_SHARE_CREATED, 'created_self', 'created_by'); + } else { + $this->addNotificationsForFileAction($path, Files_Sharing::TYPE_PUBLIC_UPLOAD, '', 'created_public'); } } @@ -807,15 +807,14 @@ protected function shareWithTeam(string $shareWith, Node $fileSource, string $fi * @throws \OCP\Files\NotFoundException */ public function unShare(IShare $share) { - if (!in_array($share->getNodeType(), ['file', 'folder'], true) || $this->isDeletedNode($share->getShareOwner(), $share->getNodeId())) { - return; - } - if ($share->getShareType() === IShare::TYPE_USER) { - $this->unshareFromUser($share); - } elseif ($share->getShareType() === IShare::TYPE_GROUP) { - $this->unshareFromGroup($share); - } elseif ($share->getShareType() === IShare::TYPE_LINK) { - $this->unshareLink($share); + if (in_array($share->getNodeType(), ['file', 'folder'], true) && !$this->isDeletedNode($share->getShareOwner(), $share->getNodeId())) { + if ($share->getShareType() === IShare::TYPE_USER) { + $this->unshareFromUser($share); + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { + $this->unshareFromGroup($share); + } elseif ($share->getShareType() === IShare::TYPE_LINK) { + $this->unshareLink($share); + } } } @@ -826,13 +825,12 @@ public function unShare(IShare $share) { * @throws \OCP\Files\NotFoundException */ public function unShareSelf(IShare $share) { - if (!in_array($share->getNodeType(), ['file', 'folder'], true)) { - return; - } - if ($share->getShareType() === IShare::TYPE_GROUP) { - $this->unshareFromSelfGroup($share); - } elseif ($share->getShareType() === IShare::TYPE_USER) { - $this->unshareFromUser($share); + if (in_array($share->getNodeType(), ['file', 'folder'], true)) { + if ($share->getShareType() === IShare::TYPE_GROUP) { + $this->unshareFromSelfGroup($share); + } elseif ($share->getShareType() === IShare::TYPE_USER) { + $this->unshareFromUser($share); + } } } diff --git a/lib/MailQueueHandler.php b/lib/MailQueueHandler.php index 3da47f717..c1795763c 100644 --- a/lib/MailQueueHandler.php +++ b/lib/MailQueueHandler.php @@ -11,6 +11,7 @@ use OCP\Activity\IManager; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Defaults; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeFormatter; use OCP\IDBConnection; @@ -51,6 +52,7 @@ public function __construct( protected IFactory $lFactory, protected IManager $activityManager, protected IValidator $richObjectValidator, + protected IAppConfig $appConfig, protected IConfig $config, protected LoggerInterface $logger, protected Data $data, @@ -70,6 +72,10 @@ public function __construct( * @return int Number of users we sent an email to */ public function sendEmails(int $limit, int $sendTime, bool $forceSending = false, ?int $restrictEmails = null): int { + if ($this->appConfig->getValueString('activity', 'enable_email', 'yes') === 'no') { + return 0; + } + // Get all users which should receive an email $affectedUsers = $this->getAffectedUsers($limit, $sendTime, $forceSending, $restrictEmails); if (empty($affectedUsers)) { @@ -150,15 +156,24 @@ protected function getAffectedUsers(?int $limit, int $latestSend, bool $forceSen if ($restrictEmails !== null) { if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) { - $query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(UserSettings::BATCH_TIME_HOURLY)))); + $query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600)))); } elseif ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) { - $query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(UserSettings::BATCH_TIME_DAILY)))); + $query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24)))); } elseif ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) { - $query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(UserSettings::BATCH_TIME_WEEKLY)))); + $query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24 * 7)))); } elseif ($restrictEmails === UserSettings::EMAIL_SEND_ASAP) { $query->where($query->expr()->eq('amq_timestamp', 'amq_latest_send')); } - return $this->fetchAffectedUsers($query); + + $result = $query->executeQuery(); + + $affectedUsers = []; + while ($row = $result->fetch()) { + $affectedUsers[] = $row['amq_affecteduser']; + } + $result->closeCursor(); + + return $affectedUsers; } if ($forceSending) { @@ -167,16 +182,14 @@ protected function getAffectedUsers(?int $limit, int $latestSend, bool $forceSen $query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend))); } - return $this->fetchAffectedUsers($query); - } - - private function fetchAffectedUsers(IQueryBuilder $query): array { $result = $query->executeQuery(); + $affectedUsers = []; while ($row = $result->fetch()) { $affectedUsers[] = $row['amq_affecteduser']; } $result->closeCursor(); + return $affectedUsers; } diff --git a/lib/UserSettings.php b/lib/UserSettings.php index afbc469e6..7eed94bf4 100644 --- a/lib/UserSettings.php +++ b/lib/UserSettings.php @@ -26,10 +26,6 @@ class UserSettings { public const EMAIL_SEND_WEEKLY = 2; public const EMAIL_SEND_ASAP = 3; - public const BATCH_TIME_HOURLY = 3600; - public const BATCH_TIME_DAILY = 3600 * 24; - public const BATCH_TIME_WEEKLY = 3600 * 24 * 7; - /** * @param IManager $manager * @param IConfig $config @@ -113,7 +109,7 @@ public function getAdminSetting($method, $type) { protected function getDefaultSetting($method, $type) { if ($method === 'setting') { if ($type === 'batchtime') { - return self::BATCH_TIME_HOURLY; + return 3600; } if ($type === 'self') { diff --git a/lib/ViewInfoCache.php b/lib/ViewInfoCache.php index c5b145d4b..621f51fc7 100644 --- a/lib/ViewInfoCache.php +++ b/lib/ViewInfoCache.php @@ -45,45 +45,48 @@ protected function findInfoById(string $user, int $fileId, string $filePath): ar 'view' => '', ]; + $notFound = false; try { $userFolder = $this->rootFolder->getUserFolder($user); $entry = $userFolder->getFirstNodeById($fileId); if ($entry === null) { throw new NotFoundException('No entries returned'); } + $cache['path'] = $userFolder->getRelativePath($entry->getPath()); $cache['is_dir'] = $entry instanceof Folder; $cache['exists'] = true; $cache['node'] = $entry; - $this->cacheId[$user][$fileId] = $cache; - return $cache; } catch (NotFoundException) { - // The file was not found in the normal view, maybe it is in the trashbin? - } + // The file was not found in the normal view, + // maybe it is in the trashbin? + try { + $userTrashBin = $this->rootFolder->get('/' . $user . '/files_trashbin'); + if (!$userTrashBin instanceof Folder) { + throw new NotFoundException('No trash bin found for user: ' . $user); + } + $entry = $userTrashBin->getFirstNodeById($fileId); + if ($entry === null) { + throw new NotFoundException('No entries returned'); + } - try { - $userTrashBin = $this->rootFolder->get('/' . $user . '/files_trashbin'); - if (!$userTrashBin instanceof Folder) { - throw new NotFoundException('No trash bin found for user: ' . $user); + $cache = [ + 'path' => $userTrashBin->getRelativePath($entry->getPath()), + 'exists' => true, + 'is_dir' => $entry instanceof Folder, + 'view' => 'trashbin', + 'node' => $entry, + ]; + } catch (NotFoundException) { + $notFound = true; } - $entry = $userTrashBin->getFirstNodeById($fileId); - if ($entry === null) { - throw new NotFoundException('No entries returned'); - } - $cache = [ - 'path' => $userTrashBin->getRelativePath($entry->getPath()), - 'exists' => true, - 'is_dir' => $entry instanceof Folder, - 'view' => 'trashbin', - 'node' => $entry, - ]; - } catch (NotFoundException) { - // Not found anywhere — cache path as null but return original filePath - $this->cacheId[$user][$fileId] = array_merge($cache, ['path' => null]); - return $cache; } $this->cacheId[$user][$fileId] = $cache; + if ($notFound) { + $this->cacheId[$user][$fileId]['path'] = null; + } + return $cache; } } diff --git a/package-lock.json b/package-lock.json index 881d93ba8..fd9de0eab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,17 +33,17 @@ "@nextcloud/cypress": "^1.0.0-beta.15", "@nextcloud/stylelint-config": "^3.2.1", "@nextcloud/vite-config": "^2.5.2", - "@testing-library/cypress": "^10.1.3", + "@testing-library/cypress": "^10.1.0", "@types/dockerode": "^4.0.1", "@vitest/coverage-v8": "^4.1.5", - "@vue/test-utils": "^2.4.10", + "@vue/test-utils": "^2.4.8", "@vue/tsconfig": "^0.9.1", - "cypress-vite": "^1.9.1", + "cypress-vite": "^1.8.0", "cypress-wait-until": "^3.0.2", "dockerode": "^5.0.0", - "eslint-plugin-cypress": "^6.4.0", + "eslint-plugin-cypress": "^6.3.1", "happy-dom": "^20.9.0", - "stylelint": "^17.9.1", + "stylelint": "^17.9.0", "typescript": "^6.0.3", "vite": "^7.3.2", "vitest": "^4.0.18", @@ -3187,11 +3187,10 @@ } }, "node_modules/@testing-library/cypress": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.1.3.tgz", - "integrity": "sha512-rVCH92TmU8idROHqCdTSp/bosIIUezihSwFfR/J2GZD0EAwyzRuYNseh54eziKJWjJ64BCXPT3X3jLO7v4yenQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.1.0.tgz", + "integrity": "sha512-tNkNtYRqPQh71xXKuMizr146zlellawUfDth7A/urYU4J66g0VGZ063YsS0gqS79Z58u1G/uo9UxN05qvKXMag==", "dev": true, - "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.6", "@testing-library/dom": "^10.1.0" @@ -3201,7 +3200,7 @@ "npm": ">=6" }, "peerDependencies": { - "cypress": ">=12" + "cypress": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" } }, "node_modules/@testing-library/cypress/node_modules/@testing-library/dom": { @@ -4134,9 +4133,9 @@ "license": "MIT" }, "node_modules/@vue/test-utils": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.10.tgz", - "integrity": "sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==", + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.8.tgz", + "integrity": "sha512-cjAKFbSXFhtZ9Cj+ug60b21lW/BN737e+Syu2LPACIW6R0zVtj65Fnfe649KjfHor3Etx3ZavDFFBrZ+p21YNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5872,17 +5871,16 @@ } }, "node_modules/cypress-vite": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/cypress-vite/-/cypress-vite-1.9.1.tgz", - "integrity": "sha512-n+hmNCfCtPoVZt9BHq6FzH6I9OhkZ83cj26sc3kgtyX/TrN4oWJQyUK4fZYkckPKUf1JlroBlEHHskpFQpclxQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/cypress-vite/-/cypress-vite-1.8.0.tgz", + "integrity": "sha512-rPkIpDzCIo+upsDkFa/NlrnzVumuQ45UcwL7a2k/n8WFIwsW8QYuQaWU2JiIKExP/LNQew3H3Hbs/bp26xC0Fw==", "dev": true, - "license": "MIT", "dependencies": { "chokidar": "^3.5.3", - "debug": "^4.4.3" + "debug": "^4.3.4" }, "peerDependencies": { - "vite": "^2.9 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "vite": "^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/cypress-wait-until": { @@ -6642,22 +6640,16 @@ } }, "node_modules/eslint-plugin-cypress": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-6.4.0.tgz", - "integrity": "sha512-zdGXFSmTH4w2A+2aU/Abna2s8Vcz5mDxDsnAXP5Vl+pERWRQxlSE2qrrmriNUEKZ62tlOA87b9AOnJX49tOhAQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-6.3.1.tgz", + "integrity": "sha512-iTJtdIZbyCUlagEI4YlVcwgPFV7X379Qi/upujaD4kvOaQkMvzmpt90vfSnaqgqprp/HPIvhnzv3fdI7mYV4QQ==", "dev": true, "license": "MIT", "dependencies": { "globals": "^17.5.0" }, "peerDependencies": { - "@typescript-eslint/parser": ">=8", "eslint": ">=9" - }, - "peerDependenciesMeta": { - "@typescript-eslint/parser": { - "optional": true - } } }, "node_modules/eslint-plugin-jsdoc": { @@ -7133,26 +7125,15 @@ "peer": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true }, "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", "funding": [ { "type": "github", @@ -7161,8 +7142,7 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "path-expression-matcher": "^1.1.3" } }, "node_modules/fast-xml-parser": { @@ -12368,9 +12348,9 @@ } }, "node_modules/stylelint": { - "version": "17.9.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.9.1.tgz", - "integrity": "sha512-THTmnAPJTrg/JhkTWZlSyrO+HUYMx6ELthIHeMyD2WOKqXIJUFQv2Yxn91bvUrZdbBJaW2dUuQdPST2wcQ6C3g==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.9.0.tgz", + "integrity": "sha512-xO0jeY6z1/urFL5L/BZLmB1yYlbRiRMQnYH6ArZIDWJ+SZXGssOY7XoYb1JIv/L220+EBnwwJXJS4Mt/F96SvA==", "dev": true, "funding": [ { @@ -14726,21 +14706,6 @@ "node": ">=12" } }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16710,9 +16675,9 @@ } }, "@testing-library/cypress": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.1.3.tgz", - "integrity": "sha512-rVCH92TmU8idROHqCdTSp/bosIIUezihSwFfR/J2GZD0EAwyzRuYNseh54eziKJWjJ64BCXPT3X3jLO7v4yenQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.1.0.tgz", + "integrity": "sha512-tNkNtYRqPQh71xXKuMizr146zlellawUfDth7A/urYU4J66g0VGZ063YsS0gqS79Z58u1G/uo9UxN05qvKXMag==", "dev": true, "requires": { "@babel/runtime": "^7.14.6", @@ -17407,9 +17372,9 @@ "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==" }, "@vue/test-utils": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.10.tgz", - "integrity": "sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==", + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.8.tgz", + "integrity": "sha512-cjAKFbSXFhtZ9Cj+ug60b21lW/BN737e+Syu2LPACIW6R0zVtj65Fnfe649KjfHor3Etx3ZavDFFBrZ+p21YNw==", "dev": true, "requires": { "js-beautify": "^1.14.9", @@ -18667,13 +18632,13 @@ } }, "cypress-vite": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/cypress-vite/-/cypress-vite-1.9.1.tgz", - "integrity": "sha512-n+hmNCfCtPoVZt9BHq6FzH6I9OhkZ83cj26sc3kgtyX/TrN4oWJQyUK4fZYkckPKUf1JlroBlEHHskpFQpclxQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/cypress-vite/-/cypress-vite-1.8.0.tgz", + "integrity": "sha512-rPkIpDzCIo+upsDkFa/NlrnzVumuQ45UcwL7a2k/n8WFIwsW8QYuQaWU2JiIKExP/LNQew3H3Hbs/bp26xC0Fw==", "dev": true, "requires": { "chokidar": "^3.5.3", - "debug": "^4.4.3" + "debug": "^4.3.4" } }, "cypress-wait-until": { @@ -19227,9 +19192,9 @@ "requires": {} }, "eslint-plugin-cypress": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-6.4.0.tgz", - "integrity": "sha512-zdGXFSmTH4w2A+2aU/Abna2s8Vcz5mDxDsnAXP5Vl+pERWRQxlSE2qrrmriNUEKZ62tlOA87b9AOnJX49tOhAQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-6.3.1.tgz", + "integrity": "sha512-iTJtdIZbyCUlagEI4YlVcwgPFV7X379Qi/upujaD4kvOaQkMvzmpt90vfSnaqgqprp/HPIvhnzv3fdI7mYV4QQ==", "dev": true, "requires": { "globals": "^17.5.0" @@ -19518,18 +19483,17 @@ "peer": true }, "fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", "dev": true }, "fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", "requires": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "path-expression-matcher": "^1.1.3" } }, "fast-xml-parser": { @@ -23082,9 +23046,9 @@ } }, "stylelint": { - "version": "17.9.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.9.1.tgz", - "integrity": "sha512-THTmnAPJTrg/JhkTWZlSyrO+HUYMx6ELthIHeMyD2WOKqXIJUFQv2Yxn91bvUrZdbBJaW2dUuQdPST2wcQ6C3g==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.9.0.tgz", + "integrity": "sha512-xO0jeY6z1/urFL5L/BZLmB1yYlbRiRMQnYH6ArZIDWJ+SZXGssOY7XoYb1JIv/L220+EBnwwJXJS4Mt/F96SvA==", "dev": true, "requires": { "@csstools/css-calc": "^3.2.0", @@ -24444,11 +24408,6 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" }, - "xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 75db6cc21..75d7d9ca8 100644 --- a/package.json +++ b/package.json @@ -60,17 +60,17 @@ "@nextcloud/cypress": "^1.0.0-beta.15", "@nextcloud/stylelint-config": "^3.2.1", "@nextcloud/vite-config": "^2.5.2", - "@testing-library/cypress": "^10.1.3", + "@testing-library/cypress": "^10.1.0", "@types/dockerode": "^4.0.1", "@vitest/coverage-v8": "^4.1.5", - "@vue/test-utils": "^2.4.10", + "@vue/test-utils": "^2.4.8", "@vue/tsconfig": "^0.9.1", - "cypress-vite": "^1.9.1", + "cypress-vite": "^1.8.0", "cypress-wait-until": "^3.0.2", "dockerode": "^5.0.0", - "eslint-plugin-cypress": "^6.4.0", + "eslint-plugin-cypress": "^6.3.1", "happy-dom": "^20.9.0", - "stylelint": "^17.9.1", + "stylelint": "^17.9.0", "typescript": "^6.0.3", "vite": "^7.3.2", "vitest": "^4.0.18", diff --git a/tests/Controller/APIv2ControllerTest.php b/tests/Controller/APIv2ControllerTest.php index b133a48db..ebdb1a147 100644 --- a/tests/Controller/APIv2ControllerTest.php +++ b/tests/Controller/APIv2ControllerTest.php @@ -140,11 +140,9 @@ public function testValidateParametersFilter(string $param, string $filter): voi ->method('validateFilter') ->with($param) ->willReturn($filter); - $userMock = $this->createMock(IUser::class); - $userMock->method('getUID')->willReturn('testuser'); $this->userSession->expects($this->once()) ->method('getUser') - ->willReturn($userMock); + ->willReturn($this->createMock(IUser::class)); self::invokePrivate($this->controller, 'validateParameters', [$param, 0, 0, false, '', 0, 'desc']); $this->assertSame($filter, self::invokePrivate($this->controller, 'filter')); @@ -175,21 +173,19 @@ public static function dataValidateParametersObject(): array { return [ ['type', 42, 'type', 42], ['type', '42', 'type', 42], - ['', 42, '', 0], - ['type', 0, '', 0], + [null, '42', '', 0], + ['type', null, '', 0], ]; } #[DataProvider('dataValidateParametersObject')] - public function testValidateParametersObject(string $type, mixed $id, string $expectedType, int $expectedId): void { + public function testValidateParametersObject(?string $type, mixed $id, string $expectedType, int $expectedId): void { $this->data->expects($this->once()) ->method('validateFilter') ->willReturnArgument(0); - $userMock = $this->createMock(IUser::class); - $userMock->method('getUID')->willReturn('testuser'); $this->userSession->expects($this->once()) ->method('getUser') - ->willReturn($userMock); + ->willReturn($this->createMock(IUser::class)); self::invokePrivate($this->controller, 'validateParameters', ['all', 0, 0, false, $type, $id, 'desc']); $this->assertSame($expectedType, self::invokePrivate($this->controller, 'objectType')); @@ -233,11 +229,9 @@ public function testValidateParameters(string $param, mixed $value, string $memb $this->data->expects($this->once()) ->method('validateFilter') ->willReturnArgument(0); - $userMock = $this->createMock(IUser::class); - $userMock->method('getUID')->willReturn('testuser'); $this->userSession->expects($this->once()) ->method('getUser') - ->willReturn($userMock); + ->willReturn($this->createMock(IUser::class)); self::invokePrivate($this->controller, 'validateParameters', $params); $this->assertSame($expectedValue, self::invokePrivate($this->controller, $memberName)); diff --git a/tests/FilesHooksTest.php b/tests/FilesHooksTest.php index 0b8dd6229..a79eecd3e 100644 --- a/tests/FilesHooksTest.php +++ b/tests/FilesHooksTest.php @@ -171,8 +171,6 @@ public static function dataFileCreate(): array { ['user', false, 'created_self', 'created_by', Files::TYPE_SHARE_CREATED], ['', true, '', 'created_public', Files_Sharing::TYPE_PUBLIC_UPLOAD], ['', false, 'created_self', 'created_by', Files::TYPE_SHARE_CREATED], - // logged-in user uploading to a public share link → treated as regular upload - ['user', true, 'created_self', 'created_by', Files::TYPE_SHARE_CREATED], ]; } @@ -1141,85 +1139,4 @@ public function testGetUserPathsFromPathSuccess(): void { $this->assertSame(['remote1' => ['token' => 'abc']], $result['remotes']); $this->assertSame('/test/path', $result['ownerPath']); } - - private function getShareMock(string $nodeType, int $shareType, string $owner = 'owner', int $nodeId = 42): IShare&MockObject { - $share = $this->createMock(IShare::class); - $share->method('getNodeType')->willReturn($nodeType); - $share->method('getShareType')->willReturn($shareType); - $share->method('getShareOwner')->willReturn($owner); - $share->method('getNodeId')->willReturn($nodeId); - return $share; - } - - private function mockNodeNotDeleted(string $owner, int $nodeId): void { - $node = $this->createMock(File::class); - $userFolder = $this->createMock(Folder::class); - $userFolder->method('getFirstNodeById')->with($nodeId)->willReturn($node); - $this->rootFolder->method('getUserFolder')->with($owner)->willReturn($userFolder); - } - - private function mockNodeDeleted(string $owner): void { - $this->rootFolder->method('getUserFolder') - ->with($owner) - ->willThrowException(new NotFoundException()); - } - - public function testUnShareIgnoresNonFileOrFolder(): void { - $filesHooks = $this->getFilesHooks(['unshareFromUser', 'unshareFromGroup', 'unshareLink']); - $share = $this->getShareMock('other', IShare::TYPE_USER); - - $filesHooks->expects($this->never())->method('unshareFromUser'); - $filesHooks->expects($this->never())->method('unshareFromGroup'); - $filesHooks->expects($this->never())->method('unshareLink'); - - $filesHooks->unShare($share); - } - - public function testUnShareIgnoresDeletedNode(): void { - $filesHooks = $this->getFilesHooks(['unshareFromUser', 'unshareFromGroup', 'unshareLink']); - $share = $this->getShareMock('file', IShare::TYPE_USER); - $this->mockNodeDeleted('owner'); - - $filesHooks->expects($this->never())->method('unshareFromUser'); - $filesHooks->expects($this->never())->method('unshareFromGroup'); - $filesHooks->expects($this->never())->method('unshareLink'); - - $filesHooks->unShare($share); - } - - public function testUnShareUser(): void { - $filesHooks = $this->getFilesHooks(['unshareFromUser', 'unshareFromGroup', 'unshareLink']); - $share = $this->getShareMock('file', IShare::TYPE_USER); - $this->mockNodeNotDeleted('owner', 42); - - $filesHooks->expects($this->once())->method('unshareFromUser')->with($share); - $filesHooks->expects($this->never())->method('unshareFromGroup'); - $filesHooks->expects($this->never())->method('unshareLink'); - - $filesHooks->unShare($share); - } - - public function testUnShareGroup(): void { - $filesHooks = $this->getFilesHooks(['unshareFromUser', 'unshareFromGroup', 'unshareLink']); - $share = $this->getShareMock('folder', IShare::TYPE_GROUP); - $this->mockNodeNotDeleted('owner', 42); - - $filesHooks->expects($this->never())->method('unshareFromUser'); - $filesHooks->expects($this->once())->method('unshareFromGroup')->with($share); - $filesHooks->expects($this->never())->method('unshareLink'); - - $filesHooks->unShare($share); - } - - public function testUnShareLink(): void { - $filesHooks = $this->getFilesHooks(['unshareFromUser', 'unshareFromGroup', 'unshareLink']); - $share = $this->getShareMock('file', IShare::TYPE_LINK); - $this->mockNodeNotDeleted('owner', 42); - - $filesHooks->expects($this->never())->method('unshareFromUser'); - $filesHooks->expects($this->never())->method('unshareFromGroup'); - $filesHooks->expects($this->once())->method('unshareLink')->with($share); - - $filesHooks->unShare($share); - } } diff --git a/tests/MailQueueHandlerTest.php b/tests/MailQueueHandlerTest.php index 91a658138..9397e9192 100644 --- a/tests/MailQueueHandlerTest.php +++ b/tests/MailQueueHandlerTest.php @@ -34,6 +34,7 @@ use OCA\Activity\UserSettings; use OCP\Activity\IEvent; use OCP\Activity\IManager; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeFormatter; use OCP\IDBConnection; @@ -65,6 +66,7 @@ class MailQueueHandlerTest extends TestCase { protected IFactory&MockObject $lFactory; protected IManager&MockObject $activityManager; protected IValidator&MockObject $richObjectValidator; + protected IAppConfig&MockObject $appConfig; protected IConfig&MockObject $config; protected MockObject&LoggerInterface $logger; @@ -81,6 +83,10 @@ protected function setUp(): void { $app = self::getUniqueID('MailQueueHandlerTest', 10); $this->userManager = $this->createMock(IUserManager::class); $this->lFactory = $this->createMock(IFactory::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->appConfig->method('getValueString') + ->with('activity', 'enable_email', 'yes') + ->willReturn('yes'); $this->config = $this->createMock(IConfig::class); $this->logger = $this->createMock(LoggerInterface::class); $this->dateTimeFormatter = $this->createMock(IDateTimeFormatter::class); @@ -141,6 +147,7 @@ protected function setUp(): void { $this->lFactory, $this->activityManager, $this->richObjectValidator, + $this->appConfig, $this->config, $this->logger, $this->data, @@ -370,6 +377,27 @@ public function testSendEmailsDeletesQueueOnSendReturnFalse(): void { } } + public function testSendEmailsSkipsWhenAdminEmailDisabled(): void { + $maxTime = 200; + + $this->appConfig->method('getValueString') + ->with('activity', 'enable_email', 'yes') + ->willReturn('no'); + + $this->mailer->expects($this->never()) + ->method('send'); + + $result = $this->mailQueueHandler->sendEmails(3, $maxTime); + + $this->assertSame(0, $result); + + // Queue must be untouched so emails can be sent if admin re-enables the toggle + foreach (['user1', 'user2', 'user3'] as $user) { + [$data,] = self::invokePrivate($this->mailQueueHandler, 'getItemsForUser', [$user, $maxTime]); + $this->assertNotEmpty($data, "Queue entries for $user must survive when email is globally disabled"); + } + } + /** * @param array $users * @param int $maxTime