JSCombinerTest.php 16 KB

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