JSCombinerTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace Test\Template;
  7. use OC\SystemConfig;
  8. use OC\Template\JSCombiner;
  9. use OCP\Files\IAppData;
  10. use OCP\Files\NotFoundException;
  11. use OCP\Files\NotPermittedException;
  12. use OCP\Files\SimpleFS\ISimpleFile;
  13. use OCP\Files\SimpleFS\ISimpleFolder;
  14. use OCP\ICache;
  15. use OCP\ICacheFactory;
  16. use OCP\IURLGenerator;
  17. use Psr\Log\LoggerInterface;
  18. class JSCombinerTest extends \Test\TestCase {
  19. /** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
  20. protected $appData;
  21. /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
  22. protected $urlGenerator;
  23. /** @var SystemConfig|\PHPUnit\Framework\MockObject\MockObject */
  24. protected $config;
  25. /** @var ICache|\PHPUnit\Framework\MockObject\MockObject */
  26. protected $depsCache;
  27. /** @var JSCombiner */
  28. protected $jsCombiner;
  29. /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
  30. protected $logger;
  31. /** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */
  32. protected $cacheFactory;
  33. protected function setUp(): void {
  34. parent::setUp();
  35. $this->appData = $this->createMock(IAppData::class);
  36. $this->urlGenerator = $this->createMock(IURLGenerator::class);
  37. $this->config = $this->createMock(SystemConfig::class);
  38. $this->cacheFactory = $this->createMock(ICacheFactory::class);
  39. $this->depsCache = $this->createMock(ICache::class);
  40. $this->cacheFactory->expects($this->atLeastOnce())
  41. ->method('createDistributed')
  42. ->willReturn($this->depsCache);
  43. $this->logger = $this->createMock(LoggerInterface::class);
  44. $this->jsCombiner = new JSCombiner(
  45. $this->appData,
  46. $this->urlGenerator,
  47. $this->cacheFactory,
  48. $this->config,
  49. $this->logger
  50. );
  51. }
  52. public function testProcessDebugMode(): void {
  53. $this->config
  54. ->expects($this->once())
  55. ->method('getValue')
  56. ->with('debug')
  57. ->willReturn(true);
  58. $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp');
  59. $this->assertFalse($actual);
  60. }
  61. public function testProcessNotInstalled(): void {
  62. $this->config
  63. ->expects($this->exactly(2))
  64. ->method('getValue')
  65. ->withConsecutive(
  66. ['debug'],
  67. ['installed']
  68. )
  69. ->willReturnOnConsecutiveCalls(
  70. false,
  71. false
  72. );
  73. $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp');
  74. $this->assertFalse($actual);
  75. }
  76. public function testProcessUncachedFileNoAppDataFolder(): void {
  77. $this->config
  78. ->expects($this->exactly(2))
  79. ->method('getValue')
  80. ->withConsecutive(
  81. ['debug'],
  82. ['installed']
  83. )
  84. ->willReturnOnConsecutiveCalls(
  85. false,
  86. true
  87. );
  88. $folder = $this->createMock(ISimpleFolder::class);
  89. $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willThrowException(new NotFoundException());
  90. $this->appData->expects($this->once())->method('newFolder')->with('awesomeapp')->willReturn($folder);
  91. $file = $this->createMock(ISimpleFile::class);
  92. $gzfile = $this->createMock(ISimpleFile::class);
  93. $fileDeps = $this->createMock(ISimpleFile::class);
  94. $folder->method('getFile')
  95. ->willReturnCallback(function ($path) use ($file, $gzfile) {
  96. if ($path === 'combine.js') {
  97. return $file;
  98. } elseif ($path === 'combine.js.deps') {
  99. throw new NotFoundException();
  100. } elseif ($path === 'combine.js.gzip') {
  101. return $gzfile;
  102. }
  103. $this->fail();
  104. });
  105. $folder->expects($this->once())
  106. ->method('newFile')
  107. ->with('combine.js.deps')
  108. ->willReturn($fileDeps);
  109. $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp');
  110. $this->assertTrue($actual);
  111. }
  112. public function testProcessUncachedFile(): void {
  113. $this->config
  114. ->expects($this->exactly(2))
  115. ->method('getValue')
  116. ->withConsecutive(
  117. ['debug'],
  118. ['installed']
  119. )
  120. ->willReturnOnConsecutiveCalls(
  121. false,
  122. true
  123. );
  124. $folder = $this->createMock(ISimpleFolder::class);
  125. $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willReturn($folder);
  126. $file = $this->createMock(ISimpleFile::class);
  127. $fileDeps = $this->createMock(ISimpleFile::class);
  128. $gzfile = $this->createMock(ISimpleFile::class);
  129. $folder->method('getFile')
  130. ->willReturnCallback(function ($path) use ($file, $gzfile) {
  131. if ($path === 'combine.js') {
  132. return $file;
  133. } elseif ($path === 'combine.js.deps') {
  134. throw new NotFoundException();
  135. } elseif ($path === 'combine.js.gzip') {
  136. return $gzfile;
  137. }
  138. $this->fail();
  139. });
  140. $folder->expects($this->once())
  141. ->method('newFile')
  142. ->with('combine.js.deps')
  143. ->willReturn($fileDeps);
  144. $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp');
  145. $this->assertTrue($actual);
  146. }
  147. public function testProcessCachedFile(): void {
  148. $this->config
  149. ->expects($this->exactly(2))
  150. ->method('getValue')
  151. ->withConsecutive(
  152. ['debug'],
  153. ['installed']
  154. )
  155. ->willReturnOnConsecutiveCalls(
  156. false,
  157. true
  158. );
  159. $folder = $this->createMock(ISimpleFolder::class);
  160. $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willReturn($folder);
  161. $file = $this->createMock(ISimpleFile::class);
  162. $fileDeps = $this->createMock(ISimpleFile::class);
  163. $fileDeps->expects($this->once())->method('getContent')->willReturn('{}');
  164. $folder->method('fileExists')
  165. ->with('combine.js')
  166. ->willReturn(true);
  167. $folder->method('getFile')
  168. ->willReturnCallback(function ($path) use ($file, $fileDeps) {
  169. if ($path === 'combine.js') {
  170. return $file;
  171. }
  172. if ($path === 'combine.js.deps') {
  173. return $fileDeps;
  174. }
  175. $this->fail();
  176. });
  177. $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp');
  178. $this->assertTrue($actual);
  179. }
  180. public function testProcessCachedFileMemcache(): void {
  181. $this->config
  182. ->expects($this->exactly(2))
  183. ->method('getValue')
  184. ->withConsecutive(
  185. ['debug'],
  186. ['installed']
  187. )
  188. ->willReturnOnConsecutiveCalls(
  189. false,
  190. true
  191. );
  192. $folder = $this->createMock(ISimpleFolder::class);
  193. $this->appData->expects($this->once())
  194. ->method('getFolder')
  195. ->with('awesomeapp')
  196. ->willReturn($folder);
  197. $folder->method('getName')
  198. ->willReturn('awesomeapp');
  199. $folder->method('fileExists')
  200. ->with('combine.js')
  201. ->willReturn(true);
  202. $file = $this->createMock(ISimpleFile::class);
  203. $this->depsCache->method('get')
  204. ->with('awesomeapp-combine.js.deps')
  205. ->willReturn('{}');
  206. $folder->method('getFile')
  207. ->willReturnCallback(function ($path) use ($file) {
  208. if ($path === 'combine.js') {
  209. return $file;
  210. }
  211. $this->fail();
  212. });
  213. $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp');
  214. $this->assertTrue($actual);
  215. }
  216. public function testIsCachedNoDepsFile(): void {
  217. $fileName = 'combine.json';
  218. $folder = $this->createMock(ISimpleFolder::class);
  219. $file = $this->createMock(ISimpleFile::class);
  220. $folder->method('getFile')
  221. ->willReturnCallback(function ($path) use ($file) {
  222. if ($path === 'combine.js') {
  223. return $file;
  224. }
  225. if ($path === 'combine.js.deps') {
  226. throw new NotFoundException();
  227. }
  228. $this->fail();
  229. });
  230. $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]);
  231. $this->assertFalse($actual);
  232. }
  233. public function testIsCachedWithNotExistingFile(): void {
  234. $fileName = 'combine.json';
  235. $folder = $this->createMock(ISimpleFolder::class);
  236. $folder->method('fileExists')
  237. ->with('combine.js')
  238. ->willReturn(true);
  239. $file = $this->createMock(ISimpleFile::class);
  240. $folder->method('getFile')
  241. ->with('combine.js.deps')
  242. ->willReturn($file);
  243. $file->expects($this->once())
  244. ->method('getContent')
  245. ->willReturn(json_encode(['/etc/certainlynotexisting/file/ihope' => 10000]));
  246. $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]);
  247. $this->assertFalse($actual);
  248. }
  249. public function testIsCachedWithOlderMtime(): void {
  250. $fileName = 'combine.json';
  251. $folder = $this->createMock(ISimpleFolder::class);
  252. $folder->method('fileExists')
  253. ->with('combine.js')
  254. ->willReturn(true);
  255. $file = $this->createMock(ISimpleFile::class);
  256. $folder->method('getFile')
  257. ->with('combine.js.deps')
  258. ->willReturn($file);
  259. $file->expects($this->once())
  260. ->method('getContent')
  261. ->willReturn(json_encode([__FILE__ => 1234]));
  262. $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]);
  263. $this->assertFalse($actual);
  264. }
  265. public function testIsCachedWithoutContent(): void {
  266. $fileName = 'combine.json';
  267. $folder = $this->createMock(ISimpleFolder::class);
  268. $folder->method('fileExists')
  269. ->with('combine.js')
  270. ->willReturn(true);
  271. $file = $this->createMock(ISimpleFile::class);
  272. $folder->method('getFile')
  273. ->with('combine.js.deps')
  274. ->willReturn($file);
  275. $file->expects($this->once())
  276. ->method('getContent')
  277. ->willReturn('');
  278. $this->logger->expects($this->once())
  279. ->method('info')
  280. ->with('JSCombiner: deps file empty: combine.js.deps');
  281. $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]);
  282. $this->assertFalse($actual);
  283. }
  284. public function testCacheNoFile(): void {
  285. $fileName = 'combine.js';
  286. $folder = $this->createMock(ISimpleFolder::class);
  287. $file = $this->createMock(ISimpleFile::class);
  288. $depsFile = $this->createMock(ISimpleFile::class);
  289. $gzFile = $this->createMock(ISimpleFile::class);
  290. $path = __DIR__ . '/data/';
  291. $folder->method('getFile')->willThrowException(new NotFoundException());
  292. $folder->method('newFile')->willReturnCallback(
  293. function ($filename) use ($file, $depsFile, $gzFile) {
  294. if ($filename === 'combine.js') {
  295. return $file;
  296. } elseif ($filename === 'combine.js.deps') {
  297. return $depsFile;
  298. } elseif ($filename === 'combine.js.gzip') {
  299. return $gzFile;
  300. }
  301. $this->fail();
  302. }
  303. );
  304. $file->expects($this->once())->method('putContent');
  305. $depsFile->expects($this->once())->method('putContent');
  306. $gzFile->expects($this->once())->method('putContent');
  307. $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]);
  308. $this->assertTrue($actual);
  309. }
  310. public function testCache(): void {
  311. $fileName = 'combine.js';
  312. $folder = $this->createMock(ISimpleFolder::class);
  313. $file = $this->createMock(ISimpleFile::class);
  314. $depsFile = $this->createMock(ISimpleFile::class);
  315. $gzFile = $this->createMock(ISimpleFile::class);
  316. $path = __DIR__ . '/data/';
  317. $folder->method('getFile')->willReturnCallback(
  318. function ($filename) use ($file, $depsFile, $gzFile) {
  319. if ($filename === 'combine.js') {
  320. return $file;
  321. } elseif ($filename === 'combine.js.deps') {
  322. return $depsFile;
  323. } elseif ($filename === 'combine.js.gzip') {
  324. return $gzFile;
  325. }
  326. $this->fail();
  327. }
  328. );
  329. $file->expects($this->once())->method('putContent');
  330. $depsFile->expects($this->once())->method('putContent');
  331. $gzFile->expects($this->once())->method('putContent');
  332. $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]);
  333. $this->assertTrue($actual);
  334. }
  335. public function testCacheNotPermittedException(): void {
  336. $fileName = 'combine.js';
  337. $folder = $this->createMock(ISimpleFolder::class);
  338. $file = $this->createMock(ISimpleFile::class);
  339. $depsFile = $this->createMock(ISimpleFile::class);
  340. $gzFile = $this->createMock(ISimpleFile::class);
  341. $path = __DIR__ . '/data/';
  342. $folder->expects($this->exactly(3))
  343. ->method('getFile')
  344. ->withConsecutive(
  345. [$fileName],
  346. [$fileName . '.deps'],
  347. [$fileName . '.gzip']
  348. )->willReturnOnConsecutiveCalls(
  349. $file,
  350. $depsFile,
  351. $gzFile
  352. );
  353. $file->expects($this->once())
  354. ->method('putContent')
  355. ->with('var a = \'hello\';
  356. var b = \'world\';
  357. ');
  358. $depsFile
  359. ->expects($this->once())
  360. ->method('putContent')
  361. ->with($this->callback(
  362. function ($content) {
  363. $deps = json_decode($content, true);
  364. return array_key_exists(__DIR__ . '/data//1.js', $deps)
  365. && array_key_exists(__DIR__ . '/data//2.js', $deps);
  366. }))
  367. ->willThrowException(new NotPermittedException());
  368. $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]);
  369. $this->assertFalse($actual);
  370. }
  371. public function testCacheSuccess(): void {
  372. $fileName = 'combine.js';
  373. $folder = $this->createMock(ISimpleFolder::class);
  374. $file = $this->createMock(ISimpleFile::class);
  375. $depsFile = $this->createMock(ISimpleFile::class);
  376. $gzFile = $this->createMock(ISimpleFile::class);
  377. $path = __DIR__ . '/data/';
  378. $folder->method('getFile')->willReturnCallback(
  379. function ($filename) use ($file, $depsFile, $gzFile) {
  380. if ($filename === 'combine.js') {
  381. return $file;
  382. } elseif ($filename === 'combine.js.deps') {
  383. return $depsFile;
  384. } elseif ($filename === 'combine.js.gzip') {
  385. return $gzFile;
  386. }
  387. $this->fail();
  388. }
  389. );
  390. $file->expects($this->once())
  391. ->method('putContent')
  392. ->with('var a = \'hello\';
  393. var b = \'world\';
  394. ');
  395. $depsFile->expects($this->once())->method('putContent')->with($this->callback(
  396. function ($content) {
  397. $deps = json_decode($content, true);
  398. return array_key_exists(__DIR__ . '/data//1.js', $deps)
  399. && array_key_exists(__DIR__ . '/data//2.js', $deps);
  400. }));
  401. $gzFile->expects($this->once())->method('putContent')->with($this->callback(
  402. function ($content) {
  403. return gzdecode($content) === 'var a = \'hello\';
  404. var b = \'world\';
  405. ';
  406. }
  407. ));
  408. $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]);
  409. $this->assertTrue($actual);
  410. }
  411. public function dataGetCachedSCSS() {
  412. return [
  413. ['awesomeapp', 'core/js/foo.json', '/js/core/foo.js'],
  414. ['files', 'apps/files/js/foo.json', '/js/files/foo.js']
  415. ];
  416. }
  417. /**
  418. * @param $appName
  419. * @param $fileName
  420. * @param $result
  421. * @dataProvider dataGetCachedSCSS
  422. */
  423. public function testGetCachedSCSS($appName, $fileName, $result): void {
  424. $this->urlGenerator->expects($this->once())
  425. ->method('linkToRoute')
  426. ->with('core.Js.getJs', [
  427. 'fileName' => 'foo.js',
  428. 'appName' => $appName
  429. ])
  430. ->willReturn(\OC::$WEBROOT . $result);
  431. $actual = $this->jsCombiner->getCachedJS($appName, $fileName);
  432. $this->assertEquals(substr($result, 1), $actual);
  433. }
  434. public function testGetContent(): void {
  435. // Create temporary file with some content
  436. $tmpFile = \OC::$server->getTempManager()->getTemporaryFile('JSCombinerTest');
  437. $pathInfo = pathinfo($tmpFile);
  438. file_put_contents($tmpFile, json_encode(['/foo/bar/test', $pathInfo['dirname'] . '/js/mytest.js']));
  439. $tmpFilePathArray = explode('/', $pathInfo['basename']);
  440. array_pop($tmpFilePathArray);
  441. $expected = [
  442. '//foo/bar/test',
  443. '/' . implode('/', $tmpFilePathArray) . $pathInfo['dirname'] . '/js/mytest.js',
  444. ];
  445. $this->assertEquals($expected, $this->jsCombiner->getContent($pathInfo['dirname'], $pathInfo['basename']));
  446. }
  447. public function testGetContentInvalidJson(): void {
  448. // Create temporary file with some content
  449. $tmpFile = \OC::$server->getTempManager()->getTemporaryFile('JSCombinerTest');
  450. $pathInfo = pathinfo($tmpFile);
  451. file_put_contents($tmpFile, 'CertainlyNotJson');
  452. $expected = [];
  453. $this->assertEquals($expected, $this->jsCombiner->getContent($pathInfo['dirname'], $pathInfo['basename']));
  454. }
  455. public function testResetCache(): void {
  456. $file = $this->createMock(ISimpleFile::class);
  457. $file->expects($this->once())
  458. ->method('delete');
  459. $folder = $this->createMock(ISimpleFolder::class);
  460. $folder->expects($this->once())
  461. ->method('getDirectoryListing')
  462. ->willReturn([$file]);
  463. $cache = $this->createMock(ICache::class);
  464. $this->cacheFactory->expects($this->once())
  465. ->method('createDistributed')
  466. ->willReturn($cache);
  467. $cache->expects($this->never())
  468. ->method('clear');
  469. $this->appData->expects($this->once())
  470. ->method('getDirectoryListing')
  471. ->willReturn([$folder]);
  472. $this->jsCombiner->resetCache();
  473. }
  474. }