You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

625 lines
17 KiB

11 years ago
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace Test;
  8. use DOMDocument;
  9. use DOMNode;
  10. use OC\Command\QueueBus;
  11. use OC\Files\Cache\Storage;
  12. use OC\Files\Config\MountProviderCollection;
  13. use OC\Files\Filesystem;
  14. use OC\Files\Mount\CacheMountProvider;
  15. use OC\Files\Mount\LocalHomeMountProvider;
  16. use OC\Files\Mount\RootMountProvider;
  17. use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
  18. use OC\Files\SetupManager;
  19. use OC\Files\View;
  20. use OC\Template\Base;
  21. use OCP\AppFramework\QueryException;
  22. use OCP\Command\IBus;
  23. use OCP\DB\QueryBuilder\IQueryBuilder;
  24. use OCP\Defaults;
  25. use OCP\IConfig;
  26. use OCP\IDBConnection;
  27. use OCP\IL10N;
  28. use OCP\IUserManager;
  29. use OCP\IUserSession;
  30. use OCP\Lock\ILockingProvider;
  31. use OCP\Lock\LockedException;
  32. use OCP\Security\ISecureRandom;
  33. use OCP\Server;
  34. if (version_compare(\PHPUnit\Runner\Version::id(), 10, '>=')) {
  35. trait OnNotSuccessfulTestTrait {
  36. protected function onNotSuccessfulTest(\Throwable $t): never {
  37. $this->restoreAllServices();
  38. // restore database connection
  39. if (!$this->IsDatabaseAccessAllowed()) {
  40. \OC::$server->registerService(IDBConnection::class, function () {
  41. return self::$realDatabase;
  42. });
  43. }
  44. parent::onNotSuccessfulTest($t);
  45. }
  46. }
  47. } else {
  48. trait OnNotSuccessfulTestTrait {
  49. protected function onNotSuccessfulTest(\Throwable $t): void {
  50. $this->restoreAllServices();
  51. // restore database connection
  52. if (!$this->IsDatabaseAccessAllowed()) {
  53. \OC::$server->registerService(IDBConnection::class, function () {
  54. return self::$realDatabase;
  55. });
  56. }
  57. parent::onNotSuccessfulTest($t);
  58. }
  59. }
  60. }
  61. abstract class TestCase extends \PHPUnit\Framework\TestCase {
  62. /** @var \OC\Command\QueueBus */
  63. private $commandBus;
  64. /** @var IDBConnection */
  65. protected static $realDatabase = null;
  66. /** @var bool */
  67. private static $wasDatabaseAllowed = false;
  68. /** @var array */
  69. protected $services = [];
  70. use OnNotSuccessfulTestTrait;
  71. /**
  72. * @param string $name
  73. * @param mixed $newService
  74. * @return bool
  75. */
  76. public function overwriteService(string $name, $newService): bool {
  77. if (isset($this->services[$name])) {
  78. return false;
  79. }
  80. try {
  81. $this->services[$name] = Server::get($name);
  82. } catch (QueryException $e) {
  83. $this->services[$name] = false;
  84. }
  85. $container = \OC::$server->getAppContainerForService($name);
  86. $container = $container ?? \OC::$server;
  87. $container->registerService($name, function () use ($newService) {
  88. return $newService;
  89. });
  90. return true;
  91. }
  92. /**
  93. * @param string $name
  94. * @return bool
  95. */
  96. public function restoreService(string $name): bool {
  97. if (isset($this->services[$name])) {
  98. $oldService = $this->services[$name];
  99. $container = \OC::$server->getAppContainerForService($name);
  100. $container = $container ?? \OC::$server;
  101. if ($oldService !== false) {
  102. $container->registerService($name, function () use ($oldService) {
  103. return $oldService;
  104. });
  105. } else {
  106. unset($container[$oldService]);
  107. }
  108. unset($this->services[$name]);
  109. return true;
  110. }
  111. return false;
  112. }
  113. public function restoreAllServices() {
  114. if (!empty($this->services)) {
  115. if (!empty($this->services)) {
  116. foreach ($this->services as $name => $service) {
  117. $this->restoreService($name);
  118. }
  119. }
  120. }
  121. }
  122. protected function getTestTraits() {
  123. $traits = [];
  124. $class = $this;
  125. do {
  126. $traits = array_merge(class_uses($class), $traits);
  127. } while ($class = get_parent_class($class));
  128. foreach ($traits as $trait => $same) {
  129. $traits = array_merge(class_uses($trait), $traits);
  130. }
  131. $traits = array_unique($traits);
  132. return array_filter($traits, function ($trait) {
  133. return substr($trait, 0, 5) === 'Test\\';
  134. });
  135. }
  136. protected function setUp(): void {
  137. // overwrite the command bus with one we can run ourselves
  138. $this->commandBus = new QueueBus();
  139. $this->overwriteService('AsyncCommandBus', $this->commandBus);
  140. $this->overwriteService(IBus::class, $this->commandBus);
  141. // detect database access
  142. self::$wasDatabaseAllowed = true;
  143. if (!$this->IsDatabaseAccessAllowed()) {
  144. self::$wasDatabaseAllowed = false;
  145. if (is_null(self::$realDatabase)) {
  146. self::$realDatabase = Server::get(IDBConnection::class);
  147. }
  148. \OC::$server->registerService(IDBConnection::class, function (): void {
  149. $this->fail('Your test case is not allowed to access the database.');
  150. });
  151. }
  152. $traits = $this->getTestTraits();
  153. foreach ($traits as $trait) {
  154. $methodName = 'setUp' . basename(str_replace('\\', '/', $trait));
  155. if (method_exists($this, $methodName)) {
  156. call_user_func([$this, $methodName]);
  157. }
  158. }
  159. }
  160. protected function tearDown(): void {
  161. $this->restoreAllServices();
  162. // restore database connection
  163. if (!$this->IsDatabaseAccessAllowed()) {
  164. \OC::$server->registerService(IDBConnection::class, function () {
  165. return self::$realDatabase;
  166. });
  167. }
  168. // further cleanup
  169. $hookExceptions = \OC_Hook::$thrownExceptions;
  170. \OC_Hook::$thrownExceptions = [];
  171. Server::get(ILockingProvider::class)->releaseAll();
  172. if (!empty($hookExceptions)) {
  173. throw $hookExceptions[0];
  174. }
  175. // fail hard if xml errors have not been cleaned up
  176. $errors = libxml_get_errors();
  177. libxml_clear_errors();
  178. if (!empty($errors)) {
  179. self::assertEquals([], $errors, 'There have been xml parsing errors');
  180. }
  181. if ($this->IsDatabaseAccessAllowed()) {
  182. Storage::getGlobalCache()->clearCache();
  183. }
  184. // tearDown the traits
  185. $traits = $this->getTestTraits();
  186. foreach ($traits as $trait) {
  187. $methodName = 'tearDown' . basename(str_replace('\\', '/', $trait));
  188. if (method_exists($this, $methodName)) {
  189. call_user_func([$this, $methodName]);
  190. }
  191. }
  192. }
  193. /**
  194. * Allows us to test private methods/properties
  195. *
  196. * @param $object
  197. * @param $methodName
  198. * @param array $parameters
  199. * @return mixed
  200. */
  201. protected static function invokePrivate($object, $methodName, array $parameters = []) {
  202. if (is_string($object)) {
  203. $className = $object;
  204. } else {
  205. $className = get_class($object);
  206. }
  207. $reflection = new \ReflectionClass($className);
  208. if ($reflection->hasMethod($methodName)) {
  209. $method = $reflection->getMethod($methodName);
  210. $method->setAccessible(true);
  211. return $method->invokeArgs($object, $parameters);
  212. } elseif ($reflection->hasProperty($methodName)) {
  213. $property = $reflection->getProperty($methodName);
  214. $property->setAccessible(true);
  215. if (!empty($parameters)) {
  216. if ($property->isStatic()) {
  217. $property->setValue(null, array_pop($parameters));
  218. } else {
  219. $property->setValue($object, array_pop($parameters));
  220. }
  221. }
  222. if (is_object($object)) {
  223. return $property->getValue($object);
  224. }
  225. return $property->getValue();
  226. } elseif ($reflection->hasConstant($methodName)) {
  227. return $reflection->getConstant($methodName);
  228. }
  229. return false;
  230. }
  231. /**
  232. * Returns a unique identifier as uniqid() is not reliable sometimes
  233. *
  234. * @param string $prefix
  235. * @param int $length
  236. * @return string
  237. */
  238. protected static function getUniqueID($prefix = '', $length = 13) {
  239. return $prefix . Server::get(ISecureRandom::class)->generate(
  240. $length,
  241. // Do not use dots and slashes as we use the value for file names
  242. ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER
  243. );
  244. }
  245. /**
  246. * Filter methods
  247. *
  248. * Returns all methods of the given class,
  249. * that are public or abstract and not in the ignoreMethods list,
  250. * to be able to fill onlyMethods() with an inverted list.
  251. *
  252. * @param string $className
  253. * @param string[] $filterMethods
  254. * @return string[]
  255. */
  256. public function filterClassMethods(string $className, array $filterMethods): array {
  257. $class = new \ReflectionClass($className);
  258. $methods = [];
  259. foreach ($class->getMethods() as $method) {
  260. if (($method->isPublic() || $method->isAbstract()) && !in_array($method->getName(), $filterMethods, true)) {
  261. $methods[] = $method->getName();
  262. }
  263. }
  264. return $methods;
  265. }
  266. public static function tearDownAfterClass(): void {
  267. if (!self::$wasDatabaseAllowed && self::$realDatabase !== null) {
  268. // in case an error is thrown in a test, PHPUnit jumps straight to tearDownAfterClass,
  269. // so we need the database again
  270. \OC::$server->registerService(IDBConnection::class, function () {
  271. return self::$realDatabase;
  272. });
  273. }
  274. $dataDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data-autotest');
  275. if (self::$wasDatabaseAllowed && Server::get(IDBConnection::class)) {
  276. $db = Server::get(IDBConnection::class);
  277. if ($db->inTransaction()) {
  278. $db->rollBack();
  279. throw new \Exception('There was a transaction still in progress and needed to be rolled back. Please fix this in your test.');
  280. }
  281. $queryBuilder = $db->getQueryBuilder();
  282. self::tearDownAfterClassCleanShares($queryBuilder);
  283. self::tearDownAfterClassCleanStorages($queryBuilder);
  284. self::tearDownAfterClassCleanFileCache($queryBuilder);
  285. }
  286. self::tearDownAfterClassCleanStrayDataFiles($dataDir);
  287. self::tearDownAfterClassCleanStrayHooks();
  288. self::tearDownAfterClassCleanStrayLocks();
  289. /** @var SetupManager $setupManager */
  290. $setupManager = Server::get(SetupManager::class);
  291. $setupManager->tearDown();
  292. /** @var MountProviderCollection $mountProviderCollection */
  293. $mountProviderCollection = Server::get(MountProviderCollection::class);
  294. $mountProviderCollection->clearProviders();
  295. /** @var IConfig $config */
  296. $config = Server::get(IConfig::class);
  297. $mountProviderCollection->registerProvider(new CacheMountProvider($config));
  298. $mountProviderCollection->registerHomeProvider(new LocalHomeMountProvider());
  299. $objectStoreConfig = Server::get(PrimaryObjectStoreConfig::class);
  300. $mountProviderCollection->registerRootProvider(new RootMountProvider($objectStoreConfig, $config));
  301. $setupManager->setupRoot();
  302. parent::tearDownAfterClass();
  303. }
  304. /**
  305. * Remove all entries from the share table
  306. *
  307. * @param IQueryBuilder $queryBuilder
  308. */
  309. protected static function tearDownAfterClassCleanShares(IQueryBuilder $queryBuilder) {
  310. $queryBuilder->delete('share')
  311. ->executeStatement();
  312. }
  313. /**
  314. * Remove all entries from the storages table
  315. *
  316. * @param IQueryBuilder $queryBuilder
  317. */
  318. protected static function tearDownAfterClassCleanStorages(IQueryBuilder $queryBuilder) {
  319. $queryBuilder->delete('storages')
  320. ->executeStatement();
  321. }
  322. /**
  323. * Remove all entries from the filecache table
  324. *
  325. * @param IQueryBuilder $queryBuilder
  326. */
  327. protected static function tearDownAfterClassCleanFileCache(IQueryBuilder $queryBuilder) {
  328. $queryBuilder->delete('filecache')
  329. ->runAcrossAllShards()
  330. ->executeStatement();
  331. }
  332. /**
  333. * Remove all unused files from the data dir
  334. *
  335. * @param string $dataDir
  336. */
  337. protected static function tearDownAfterClassCleanStrayDataFiles($dataDir) {
  338. $knownEntries = [
  339. 'nextcloud.log' => true,
  340. 'audit.log' => true,
  341. 'owncloud.db' => true,
  342. '.ocdata' => true,
  343. '..' => true,
  344. '.' => true,
  345. ];
  346. if ($dh = opendir($dataDir)) {
  347. while (($file = readdir($dh)) !== false) {
  348. if (!isset($knownEntries[$file])) {
  349. self::tearDownAfterClassCleanStrayDataUnlinkDir($dataDir . '/' . $file);
  350. }
  351. }
  352. closedir($dh);
  353. }
  354. }
  355. /**
  356. * Recursive delete files and folders from a given directory
  357. *
  358. * @param string $dir
  359. */
  360. protected static function tearDownAfterClassCleanStrayDataUnlinkDir($dir) {
  361. if ($dh = @opendir($dir)) {
  362. while (($file = readdir($dh)) !== false) {
  363. if (Filesystem::isIgnoredDir($file)) {
  364. continue;
  365. }
  366. $path = $dir . '/' . $file;
  367. if (is_dir($path)) {
  368. self::tearDownAfterClassCleanStrayDataUnlinkDir($path);
  369. } else {
  370. @unlink($path);
  371. }
  372. }
  373. closedir($dh);
  374. }
  375. @rmdir($dir);
  376. }
  377. /**
  378. * Clean up the list of hooks
  379. */
  380. protected static function tearDownAfterClassCleanStrayHooks() {
  381. \OC_Hook::clear();
  382. }
  383. /**
  384. * Clean up the list of locks
  385. */
  386. protected static function tearDownAfterClassCleanStrayLocks() {
  387. Server::get(ILockingProvider::class)->releaseAll();
  388. }
  389. /**
  390. * Login and setup FS as a given user,
  391. * sets the given user as the current user.
  392. *
  393. * @param string $user user id or empty for a generic FS
  394. */
  395. protected static function loginAsUser($user = '') {
  396. self::logout();
  397. Filesystem::tearDown();
  398. \OC_User::setUserId($user);
  399. $userObject = Server::get(IUserManager::class)->get($user);
  400. if (!is_null($userObject)) {
  401. $userObject->updateLastLoginTimestamp();
  402. }
  403. \OC_Util::setupFS($user);
  404. if (Server::get(IUserManager::class)->userExists($user)) {
  405. \OC::$server->getUserFolder($user);
  406. }
  407. }
  408. /**
  409. * Logout the current user and tear down the filesystem.
  410. */
  411. protected static function logout() {
  412. \OC_Util::tearDownFS();
  413. \OC_User::setUserId('');
  414. // needed for fully logout
  415. Server::get(IUserSession::class)->setUser(null);
  416. }
  417. /**
  418. * Run all commands pushed to the bus
  419. */
  420. protected function runCommands() {
  421. // get the user for which the fs is setup
  422. $view = Filesystem::getView();
  423. if ($view) {
  424. [, $user] = explode('/', $view->getRoot());
  425. } else {
  426. $user = null;
  427. }
  428. \OC_Util::tearDownFS(); // command can't reply on the fs being setup
  429. $this->commandBus->run();
  430. \OC_Util::tearDownFS();
  431. if ($user) {
  432. \OC_Util::setupFS($user);
  433. }
  434. }
  435. /**
  436. * Check if the given path is locked with a given type
  437. *
  438. * @param View $view view
  439. * @param string $path path to check
  440. * @param int $type lock type
  441. * @param bool $onMountPoint true to check the mount point instead of the
  442. * mounted storage
  443. *
  444. * @return boolean true if the file is locked with the
  445. * given type, false otherwise
  446. */
  447. protected function isFileLocked($view, $path, $type, $onMountPoint = false) {
  448. // Note: this seems convoluted but is necessary because
  449. // the format of the lock key depends on the storage implementation
  450. // (in our case mostly md5)
  451. if ($type === ILockingProvider::LOCK_SHARED) {
  452. // to check if the file has a shared lock, try acquiring an exclusive lock
  453. $checkType = ILockingProvider::LOCK_EXCLUSIVE;
  454. } else {
  455. // a shared lock cannot be set if exclusive lock is in place
  456. $checkType = ILockingProvider::LOCK_SHARED;
  457. }
  458. try {
  459. $view->lockFile($path, $checkType, $onMountPoint);
  460. // no exception, which means the lock of $type is not set
  461. // clean up
  462. $view->unlockFile($path, $checkType, $onMountPoint);
  463. return false;
  464. } catch (LockedException $e) {
  465. // we could not acquire the counter-lock, which means
  466. // the lock of $type was in place
  467. return true;
  468. }
  469. }
  470. protected function getGroupAnnotations(): array {
  471. if (method_exists($this, 'getAnnotations')) {
  472. $annotations = $this->getAnnotations();
  473. return $annotations['class']['group'] ?? [];
  474. }
  475. $r = new \ReflectionClass($this);
  476. $doc = $r->getDocComment();
  477. preg_match_all('#@group\s+(.*?)\n#s', $doc, $annotations);
  478. return $annotations[1] ?? [];
  479. }
  480. protected function IsDatabaseAccessAllowed() {
  481. $annotations = $this->getGroupAnnotations();
  482. if (isset($annotations)) {
  483. if (in_array('DB', $annotations) || in_array('SLOWDB', $annotations)) {
  484. return true;
  485. }
  486. }
  487. return false;
  488. }
  489. /**
  490. * @param string $expectedHtml
  491. * @param string $template
  492. * @param array $vars
  493. */
  494. protected function assertTemplate($expectedHtml, $template, $vars = []) {
  495. $requestToken = 12345;
  496. /** @var Defaults|\PHPUnit\Framework\MockObject\MockObject $l10n */
  497. $theme = $this->getMockBuilder('\OCP\Defaults')
  498. ->disableOriginalConstructor()->getMock();
  499. $theme->expects($this->any())
  500. ->method('getName')
  501. ->willReturn('Nextcloud');
  502. /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */
  503. $l10n = $this->getMockBuilder(IL10N::class)
  504. ->disableOriginalConstructor()->getMock();
  505. $l10n
  506. ->expects($this->any())
  507. ->method('t')
  508. ->willReturnCallback(function ($text, $parameters = []) {
  509. return vsprintf($text, $parameters);
  510. });
  511. $t = new Base($template, $requestToken, $l10n, $theme);
  512. $buf = $t->fetchPage($vars);
  513. $this->assertHtmlStringEqualsHtmlString($expectedHtml, $buf);
  514. }
  515. /**
  516. * @param string $expectedHtml
  517. * @param string $actualHtml
  518. * @param string $message
  519. */
  520. protected function assertHtmlStringEqualsHtmlString($expectedHtml, $actualHtml, $message = '') {
  521. $expected = new DOMDocument();
  522. $expected->preserveWhiteSpace = false;
  523. $expected->formatOutput = true;
  524. $expected->loadHTML($expectedHtml);
  525. $actual = new DOMDocument();
  526. $actual->preserveWhiteSpace = false;
  527. $actual->formatOutput = true;
  528. $actual->loadHTML($actualHtml);
  529. $this->removeWhitespaces($actual);
  530. $expectedHtml1 = $expected->saveHTML();
  531. $actualHtml1 = $actual->saveHTML();
  532. self::assertEquals($expectedHtml1, $actualHtml1, $message);
  533. }
  534. private function removeWhitespaces(DOMNode $domNode) {
  535. foreach ($domNode->childNodes as $node) {
  536. if ($node->hasChildNodes()) {
  537. $this->removeWhitespaces($node);
  538. } else {
  539. if ($node instanceof \DOMText && $node->isWhitespaceInElementContent()) {
  540. $domNode->removeChild($node);
  541. }
  542. }
  543. }
  544. }
  545. }