FetcherBase.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace Test\App\AppStore\Fetcher;
  7. use OC\App\AppStore\Fetcher\Fetcher;
  8. use OC\Files\AppData\AppData;
  9. use OC\Files\AppData\Factory;
  10. use OCP\AppFramework\Utility\ITimeFactory;
  11. use OCP\Files\IAppData;
  12. use OCP\Files\NotFoundException;
  13. use OCP\Files\SimpleFS\ISimpleFile;
  14. use OCP\Files\SimpleFS\ISimpleFolder;
  15. use OCP\Http\Client\IClient;
  16. use OCP\Http\Client\IClientService;
  17. use OCP\Http\Client\IResponse;
  18. use OCP\IConfig;
  19. use OCP\Support\Subscription\IRegistry;
  20. use Psr\Log\LoggerInterface;
  21. use Test\TestCase;
  22. abstract class FetcherBase extends TestCase {
  23. /** @var Factory|\PHPUnit\Framework\MockObject\MockObject */
  24. protected $appDataFactory;
  25. /** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
  26. protected $appData;
  27. /** @var IClientService|\PHPUnit\Framework\MockObject\MockObject */
  28. protected $clientService;
  29. /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
  30. protected $timeFactory;
  31. /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
  32. protected $config;
  33. /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
  34. protected $logger;
  35. /** @var IRegistry|\PHPUnit\Framework\MockObject\MockObject */
  36. protected $registry;
  37. /** @var Fetcher */
  38. protected $fetcher;
  39. /** @var string */
  40. protected $fileName;
  41. /** @var string */
  42. protected $endpoint;
  43. protected function setUp(): void {
  44. parent::setUp();
  45. $this->appDataFactory = $this->createMock(Factory::class);
  46. $this->appData = $this->createMock(AppData::class);
  47. $this->appDataFactory->expects($this->once())
  48. ->method('get')
  49. ->with('appstore')
  50. ->willReturn($this->appData);
  51. $this->clientService = $this->createMock(IClientService::class);
  52. $this->timeFactory = $this->createMock(ITimeFactory::class);
  53. $this->config = $this->createMock(IConfig::class);
  54. $this->logger = $this->createMock(LoggerInterface::class);
  55. $this->registry = $this->createMock(IRegistry::class);
  56. }
  57. public function testGetWithAlreadyExistingFileAndUpToDateTimestampAndVersion(): void {
  58. $this->config
  59. ->method('getSystemValueString')
  60. ->willReturnCallback(function ($var, $default) {
  61. if ($var === 'version') {
  62. return '11.0.0.2';
  63. }
  64. return $default;
  65. });
  66. $this->config->method('getSystemValueBool')
  67. ->willReturnArgument(1);
  68. $folder = $this->createMock(ISimpleFolder::class);
  69. $file = $this->createMock(ISimpleFile::class);
  70. $this->appData
  71. ->expects($this->once())
  72. ->method('getFolder')
  73. ->with('/')
  74. ->willReturn($folder);
  75. $folder
  76. ->expects($this->once())
  77. ->method('getFile')
  78. ->with($this->fileName)
  79. ->willReturn($file);
  80. $file
  81. ->expects($this->once())
  82. ->method('getContent')
  83. ->willReturn('{"timestamp":1200,"data":[{"id":"MyApp"}],"ncversion":"11.0.0.2"}');
  84. $this->timeFactory
  85. ->expects($this->once())
  86. ->method('getTime')
  87. ->willReturn(1499);
  88. $expected = [
  89. [
  90. 'id' => 'MyApp',
  91. ],
  92. ];
  93. $this->assertSame($expected, $this->fetcher->get());
  94. }
  95. public function testGetWithNotExistingFileAndUpToDateTimestampAndVersion(): void {
  96. $this->config
  97. ->method('getSystemValueString')
  98. ->willReturnCallback(function ($var, $default) {
  99. if ($var === 'appstoreurl') {
  100. return 'https://apps.nextcloud.com/api/v1';
  101. } elseif ($var === 'version') {
  102. return '11.0.0.2';
  103. }
  104. return $default;
  105. });
  106. $this->config->method('getSystemValueBool')
  107. ->willReturnArgument(1);
  108. $folder = $this->createMock(ISimpleFolder::class);
  109. $file = $this->createMock(ISimpleFile::class);
  110. $this->appData
  111. ->expects($this->once())
  112. ->method('getFolder')
  113. ->with('/')
  114. ->willReturn($folder);
  115. $folder
  116. ->expects($this->once())
  117. ->method('getFile')
  118. ->with($this->fileName)
  119. ->willThrowException(new NotFoundException());
  120. $folder
  121. ->expects($this->once())
  122. ->method('newFile')
  123. ->with($this->fileName)
  124. ->willReturn($file);
  125. $client = $this->createMock(IClient::class);
  126. $this->clientService
  127. ->expects($this->once())
  128. ->method('newClient')
  129. ->willReturn($client);
  130. $response = $this->createMock(IResponse::class);
  131. $client
  132. ->expects($this->once())
  133. ->method('get')
  134. ->with($this->endpoint)
  135. ->willReturn($response);
  136. $response
  137. ->expects($this->once())
  138. ->method('getBody')
  139. ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]');
  140. $response->method('getHeader')
  141. ->with($this->equalTo('ETag'))
  142. ->willReturn('"myETag"');
  143. $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502,"ncversion":"11.0.0.2","ETag":"\"myETag\""}';
  144. $file
  145. ->expects($this->once())
  146. ->method('putContent')
  147. ->with($fileData);
  148. $file
  149. ->expects($this->once())
  150. ->method('getContent')
  151. ->willReturn($fileData);
  152. $this->timeFactory
  153. ->expects($this->once())
  154. ->method('getTime')
  155. ->willReturn(1502);
  156. $expected = [
  157. [
  158. 'id' => 'MyNewApp',
  159. 'foo' => 'foo',
  160. ],
  161. [
  162. 'id' => 'bar',
  163. ],
  164. ];
  165. $this->assertSame($expected, $this->fetcher->get());
  166. }
  167. public function testGetWithAlreadyExistingFileAndOutdatedTimestamp(): void {
  168. $this->config->method('getSystemValueString')
  169. ->willReturnCallback(function ($key, $default) {
  170. if ($key === 'version') {
  171. return '11.0.0.2';
  172. } else {
  173. return $default;
  174. }
  175. });
  176. $this->config->method('getSystemValueBool')
  177. ->willReturnArgument(1);
  178. $folder = $this->createMock(ISimpleFolder::class);
  179. $file = $this->createMock(ISimpleFile::class);
  180. $this->appData
  181. ->expects($this->once())
  182. ->method('getFolder')
  183. ->with('/')
  184. ->willReturn($folder);
  185. $folder
  186. ->expects($this->once())
  187. ->method('getFile')
  188. ->with($this->fileName)
  189. ->willReturn($file);
  190. $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502,"ncversion":"11.0.0.2","ETag":"\"myETag\""}';
  191. $file
  192. ->expects($this->once())
  193. ->method('putContent')
  194. ->with($fileData);
  195. $file
  196. ->expects($this->exactly(2))
  197. ->method('getContent')
  198. ->willReturnOnConsecutiveCalls(
  199. '{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}},"ncversion":"11.0.0.2"}',
  200. $fileData
  201. );
  202. $this->timeFactory
  203. ->expects($this->exactly(2))
  204. ->method('getTime')
  205. ->willReturnOnConsecutiveCalls(
  206. 4801,
  207. 1502
  208. );
  209. $client = $this->createMock(IClient::class);
  210. $this->clientService
  211. ->expects($this->once())
  212. ->method('newClient')
  213. ->willReturn($client);
  214. $response = $this->createMock(IResponse::class);
  215. $client
  216. ->expects($this->once())
  217. ->method('get')
  218. ->with($this->endpoint)
  219. ->willReturn($response);
  220. $response
  221. ->expects($this->once())
  222. ->method('getBody')
  223. ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]');
  224. $response->method('getHeader')
  225. ->with($this->equalTo('ETag'))
  226. ->willReturn('"myETag"');
  227. $expected = [
  228. [
  229. 'id' => 'MyNewApp',
  230. 'foo' => 'foo',
  231. ],
  232. [
  233. 'id' => 'bar',
  234. ],
  235. ];
  236. $this->assertSame($expected, $this->fetcher->get());
  237. }
  238. public function testGetWithAlreadyExistingFileAndNoVersion(): void {
  239. $this->config
  240. ->method('getSystemValueString')
  241. ->willReturnCallback(function ($var, $default) {
  242. if ($var === 'appstoreurl') {
  243. return 'https://apps.nextcloud.com/api/v1';
  244. } elseif ($var === 'version') {
  245. return '11.0.0.2';
  246. }
  247. return $default;
  248. });
  249. $this->config->method('getSystemValueBool')
  250. ->willReturnArgument(1);
  251. $folder = $this->createMock(ISimpleFolder::class);
  252. $file = $this->createMock(ISimpleFile::class);
  253. $this->appData
  254. ->expects($this->once())
  255. ->method('getFolder')
  256. ->with('/')
  257. ->willReturn($folder);
  258. $folder
  259. ->expects($this->once())
  260. ->method('getFile')
  261. ->with($this->fileName)
  262. ->willReturn($file);
  263. $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1201,"ncversion":"11.0.0.2","ETag":"\"myETag\""}';
  264. $file
  265. ->expects($this->once())
  266. ->method('putContent')
  267. ->with($fileData);
  268. $file
  269. ->expects($this->exactly(2))
  270. ->method('getContent')
  271. ->willReturnOnConsecutiveCalls(
  272. '{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}}',
  273. $fileData
  274. );
  275. $this->timeFactory
  276. ->expects($this->once())
  277. ->method('getTime')
  278. ->willReturn(1201);
  279. $client = $this->createMock(IClient::class);
  280. $this->clientService
  281. ->expects($this->once())
  282. ->method('newClient')
  283. ->willReturn($client);
  284. $response = $this->createMock(IResponse::class);
  285. $client
  286. ->expects($this->once())
  287. ->method('get')
  288. ->with($this->endpoint)
  289. ->willReturn($response);
  290. $response
  291. ->expects($this->once())
  292. ->method('getBody')
  293. ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]');
  294. $response->method('getHeader')
  295. ->with($this->equalTo('ETag'))
  296. ->willReturn('"myETag"');
  297. $expected = [
  298. [
  299. 'id' => 'MyNewApp',
  300. 'foo' => 'foo',
  301. ],
  302. [
  303. 'id' => 'bar',
  304. ],
  305. ];
  306. $this->assertSame($expected, $this->fetcher->get());
  307. }
  308. public function testGetWithAlreadyExistingFileAndOutdatedVersion(): void {
  309. $this->config
  310. ->method('getSystemValueString')
  311. ->willReturnCallback(function ($var, $default) {
  312. if ($var === 'appstoreurl') {
  313. return 'https://apps.nextcloud.com/api/v1';
  314. } elseif ($var === 'version') {
  315. return '11.0.0.2';
  316. }
  317. return $default;
  318. });
  319. $this->config->method('getSystemValueBool')
  320. ->willReturnArgument(1);
  321. $folder = $this->createMock(ISimpleFolder::class);
  322. $file = $this->createMock(ISimpleFile::class);
  323. $this->appData
  324. ->expects($this->once())
  325. ->method('getFolder')
  326. ->with('/')
  327. ->willReturn($folder);
  328. $folder
  329. ->expects($this->once())
  330. ->method('getFile')
  331. ->with($this->fileName)
  332. ->willReturn($file);
  333. $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1201,"ncversion":"11.0.0.2","ETag":"\"myETag\""}';
  334. $file
  335. ->expects($this->once())
  336. ->method('putContent')
  337. ->with($fileData);
  338. $file
  339. ->expects($this->exactly(2))
  340. ->method('getContent')
  341. ->willReturnOnConsecutiveCalls(
  342. '{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}},"ncversion":"11.0.0.1"',
  343. $fileData
  344. );
  345. $this->timeFactory
  346. ->method('getTime')
  347. ->willReturn(1201);
  348. $client = $this->createMock(IClient::class);
  349. $this->clientService
  350. ->expects($this->once())
  351. ->method('newClient')
  352. ->willReturn($client);
  353. $response = $this->createMock(IResponse::class);
  354. $client
  355. ->expects($this->once())
  356. ->method('get')
  357. ->with($this->endpoint)
  358. ->willReturn($response);
  359. $response
  360. ->expects($this->once())
  361. ->method('getBody')
  362. ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]');
  363. $response->method('getHeader')
  364. ->with($this->equalTo('ETag'))
  365. ->willReturn('"myETag"');
  366. $expected = [
  367. [
  368. 'id' => 'MyNewApp',
  369. 'foo' => 'foo',
  370. ],
  371. [
  372. 'id' => 'bar',
  373. ],
  374. ];
  375. $this->assertSame($expected, $this->fetcher->get());
  376. }
  377. public function testGetWithExceptionInClient(): void {
  378. $this->config->method('getSystemValueString')
  379. ->willReturnArgument(1);
  380. $this->config->method('getSystemValueBool')
  381. ->willReturnArgument(1);
  382. $folder = $this->createMock(ISimpleFolder::class);
  383. $file = $this->createMock(ISimpleFile::class);
  384. $this->appData
  385. ->expects($this->once())
  386. ->method('getFolder')
  387. ->with('/')
  388. ->willReturn($folder);
  389. $folder
  390. ->expects($this->once())
  391. ->method('getFile')
  392. ->with($this->fileName)
  393. ->willReturn($file);
  394. $file
  395. ->expects($this->once())
  396. ->method('getContent')
  397. ->willReturn('{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}}}');
  398. $client = $this->createMock(IClient::class);
  399. $this->clientService
  400. ->expects($this->once())
  401. ->method('newClient')
  402. ->willReturn($client);
  403. $client
  404. ->expects($this->once())
  405. ->method('get')
  406. ->with($this->endpoint)
  407. ->willThrowException(new \Exception());
  408. $this->assertSame([], $this->fetcher->get());
  409. }
  410. public function testGetMatchingETag(): void {
  411. $this->config->method('getSystemValueString')
  412. ->willReturnCallback(function ($key, $default) {
  413. if ($key === 'version') {
  414. return '11.0.0.2';
  415. } else {
  416. return $default;
  417. }
  418. });
  419. $this->config->method('getSystemValueBool')
  420. ->willReturnArgument(1);
  421. $folder = $this->createMock(ISimpleFolder::class);
  422. $file = $this->createMock(ISimpleFile::class);
  423. $this->appData
  424. ->expects($this->once())
  425. ->method('getFolder')
  426. ->with('/')
  427. ->willReturn($folder);
  428. $folder
  429. ->expects($this->once())
  430. ->method('getFile')
  431. ->with($this->fileName)
  432. ->willReturn($file);
  433. $origData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1200,"ncversion":"11.0.0.2","ETag":"\"myETag\""}';
  434. $newData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":4802,"ncversion":"11.0.0.2","ETag":"\"myETag\""}';
  435. $file
  436. ->expects($this->once())
  437. ->method('putContent')
  438. ->with($newData);
  439. $file
  440. ->expects($this->exactly(2))
  441. ->method('getContent')
  442. ->willReturnOnConsecutiveCalls(
  443. $origData,
  444. $newData,
  445. );
  446. $this->timeFactory
  447. ->expects($this->exactly(2))
  448. ->method('getTime')
  449. ->willReturnOnConsecutiveCalls(
  450. 4801,
  451. 4802
  452. );
  453. $client = $this->createMock(IClient::class);
  454. $this->clientService
  455. ->expects($this->once())
  456. ->method('newClient')
  457. ->willReturn($client);
  458. $response = $this->createMock(IResponse::class);
  459. $client
  460. ->expects($this->once())
  461. ->method('get')
  462. ->with(
  463. $this->equalTo($this->endpoint),
  464. $this->equalTo([
  465. 'timeout' => 60,
  466. 'headers' => [
  467. 'If-None-Match' => '"myETag"'
  468. ]
  469. ])
  470. )->willReturn($response);
  471. $response->method('getStatusCode')
  472. ->willReturn(304);
  473. $expected = [
  474. [
  475. 'id' => 'MyNewApp',
  476. 'foo' => 'foo',
  477. ],
  478. [
  479. 'id' => 'bar',
  480. ],
  481. ];
  482. $this->assertSame($expected, $this->fetcher->get());
  483. }
  484. public function testGetNoMatchingETag(): void {
  485. $this->config->method('getSystemValueString')
  486. ->willReturnCallback(function ($key, $default) {
  487. if ($key === 'version') {
  488. return '11.0.0.2';
  489. } else {
  490. return $default;
  491. }
  492. });
  493. $this->config->method('getSystemValueBool')
  494. ->willReturnArgument(1);
  495. $folder = $this->createMock(ISimpleFolder::class);
  496. $file = $this->createMock(ISimpleFile::class);
  497. $this->appData
  498. ->expects($this->once())
  499. ->method('getFolder')
  500. ->with('/')
  501. ->willReturn($folder);
  502. $folder
  503. ->expects($this->once())
  504. ->method('getFile')
  505. ->with($this->fileName)
  506. ->willReturn($file);
  507. $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":4802,"ncversion":"11.0.0.2","ETag":"\"newETag\""}';
  508. $file
  509. ->expects($this->once())
  510. ->method('putContent')
  511. ->with($fileData);
  512. $file
  513. ->expects($this->exactly(2))
  514. ->method('getContent')
  515. ->willReturnOnConsecutiveCalls(
  516. '{"data":[{"id":"MyOldApp","abc":"def"}],"timestamp":1200,"ncversion":"11.0.0.2","ETag":"\"myETag\""}',
  517. $fileData,
  518. );
  519. $this->timeFactory
  520. ->expects($this->exactly(2))
  521. ->method('getTime')
  522. ->willReturnOnConsecutiveCalls(
  523. 4801,
  524. 4802,
  525. );
  526. $client = $this->createMock(IClient::class);
  527. $this->clientService
  528. ->expects($this->once())
  529. ->method('newClient')
  530. ->willReturn($client);
  531. $response = $this->createMock(IResponse::class);
  532. $client
  533. ->expects($this->once())
  534. ->method('get')
  535. ->with(
  536. $this->equalTo($this->endpoint),
  537. $this->equalTo([
  538. 'timeout' => 60,
  539. 'headers' => [
  540. 'If-None-Match' => '"myETag"',
  541. ]
  542. ])
  543. )
  544. ->willReturn($response);
  545. $response->method('getStatusCode')
  546. ->willReturn(200);
  547. $response
  548. ->expects($this->once())
  549. ->method('getBody')
  550. ->willReturn('[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}]');
  551. $response->method('getHeader')
  552. ->with($this->equalTo('ETag'))
  553. ->willReturn('"newETag"');
  554. $expected = [
  555. [
  556. 'id' => 'MyNewApp',
  557. 'foo' => 'foo',
  558. ],
  559. [
  560. 'id' => 'bar',
  561. ],
  562. ];
  563. $this->assertSame($expected, $this->fetcher->get());
  564. }
  565. public function testFetchAfterUpgradeNoETag(): void {
  566. $this->config->method('getSystemValueString')
  567. ->willReturnCallback(function ($key, $default) {
  568. if ($key === 'version') {
  569. return '11.0.0.3';
  570. } else {
  571. return $default;
  572. }
  573. });
  574. $this->config->method('getSystemValueBool')
  575. ->willReturnArgument(1);
  576. $folder = $this->createMock(ISimpleFolder::class);
  577. $file = $this->createMock(ISimpleFile::class);
  578. $this->appData
  579. ->expects($this->once())
  580. ->method('getFolder')
  581. ->with('/')
  582. ->willReturn($folder);
  583. $folder
  584. ->expects($this->once())
  585. ->method('getFile')
  586. ->with($this->fileName)
  587. ->willReturn($file);
  588. $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1501,"ncversion":"11.0.0.3","ETag":"\"newETag\""}';
  589. $file
  590. ->expects($this->once())
  591. ->method('putContent')
  592. ->with($fileData);
  593. $file
  594. ->expects($this->exactly(2))
  595. ->method('getContent')
  596. ->willReturnOnConsecutiveCalls(
  597. '{"data":[{"id":"MyOldApp","abc":"def"}],"timestamp":1200,"ncversion":"11.0.0.2","ETag":"\"myETag\""}',
  598. $fileData
  599. );
  600. $client = $this->createMock(IClient::class);
  601. $this->clientService
  602. ->expects($this->once())
  603. ->method('newClient')
  604. ->willReturn($client);
  605. $response = $this->createMock(IResponse::class);
  606. $client
  607. ->expects($this->once())
  608. ->method('get')
  609. ->with(
  610. $this->equalTo($this->endpoint),
  611. $this->equalTo([
  612. 'timeout' => 60,
  613. ])
  614. )
  615. ->willReturn($response);
  616. $response->method('getStatusCode')
  617. ->willReturn(200);
  618. $response
  619. ->expects($this->once())
  620. ->method('getBody')
  621. ->willReturn('[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}]');
  622. $response->method('getHeader')
  623. ->with($this->equalTo('ETag'))
  624. ->willReturn('"newETag"');
  625. $this->timeFactory
  626. ->expects($this->once())
  627. ->method('getTime')
  628. ->willReturn(1501);
  629. $expected = [
  630. [
  631. 'id' => 'MyNewApp',
  632. 'foo' => 'foo',
  633. ],
  634. [
  635. 'id' => 'bar',
  636. ],
  637. ];
  638. $this->assertSame($expected, $this->fetcher->get());
  639. }
  640. }