RefreshWebcalServiceTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author eleith <online+github@eleith.com>
  7. * @author Georg Ehrke <oc.list@georgehrke.com>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Thomas Citharel <nextcloud@tcit.fr>
  10. *
  11. * @license GNU AGPL version 3 or any later version
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU Affero General Public License as
  15. * published by the Free Software Foundation, either version 3 of the
  16. * License, or (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *
  26. */
  27. namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
  28. use GuzzleHttp\HandlerStack;
  29. use OCA\DAV\CalDAV\CalDavBackend;
  30. use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
  31. use OCP\Http\Client\IClient;
  32. use OCP\Http\Client\IClientService;
  33. use OCP\Http\Client\IResponse;
  34. use OCP\Http\Client\LocalServerException;
  35. use OCP\IConfig;
  36. use PHPUnit\Framework\MockObject\MockObject;
  37. use Psr\Log\LoggerInterface;
  38. use Sabre\DAV\Exception\BadRequest;
  39. use Sabre\VObject;
  40. use Sabre\VObject\Recur\NoInstancesException;
  41. use Test\TestCase;
  42. class RefreshWebcalServiceTest extends TestCase {
  43. /** @var CalDavBackend | MockObject */
  44. private $caldavBackend;
  45. /** @var IClientService | MockObject */
  46. private $clientService;
  47. /** @var IConfig | MockObject */
  48. private $config;
  49. /** @var LoggerInterface | MockObject */
  50. private $logger;
  51. protected function setUp(): void {
  52. parent::setUp();
  53. $this->caldavBackend = $this->createMock(CalDavBackend::class);
  54. $this->clientService = $this->createMock(IClientService::class);
  55. $this->config = $this->createMock(IConfig::class);
  56. $this->logger = $this->createMock(LoggerInterface::class);
  57. }
  58. /**
  59. * @param string $body
  60. * @param string $contentType
  61. * @param string $result
  62. *
  63. * @dataProvider runDataProvider
  64. */
  65. public function testRun(string $body, string $contentType, string $result): void {
  66. $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
  67. ->onlyMethods(['getRandomCalendarObjectUri'])
  68. ->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
  69. ->getMock();
  70. $refreshWebcalService
  71. ->method('getRandomCalendarObjectUri')
  72. ->willReturn('uri-1.ics');
  73. $this->caldavBackend->expects($this->once())
  74. ->method('getSubscriptionsForUser')
  75. ->with('principals/users/testuser')
  76. ->willReturn([
  77. [
  78. 'id' => '99',
  79. 'uri' => 'sub456',
  80. '{http://apple.com/ns/ical/}refreshrate' => 'P1D',
  81. '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
  82. '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
  83. '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
  84. 'source' => 'webcal://foo.bar/bla'
  85. ],
  86. [
  87. 'id' => '42',
  88. 'uri' => 'sub123',
  89. '{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
  90. '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
  91. '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
  92. '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
  93. 'source' => 'webcal://foo.bar/bla2'
  94. ],
  95. ]);
  96. $client = $this->createMock(IClient::class);
  97. $response = $this->createMock(IResponse::class);
  98. $this->clientService->expects($this->once())
  99. ->method('newClient')
  100. ->with()
  101. ->willReturn($client);
  102. $this->config->expects($this->once())
  103. ->method('getAppValue')
  104. ->with('dav', 'webcalAllowLocalAccess', 'no')
  105. ->willReturn('no');
  106. $client->expects($this->once())
  107. ->method('get')
  108. ->with('https://foo.bar/bla2', $this->callback(function ($obj) {
  109. return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
  110. }))
  111. ->willReturn($response);
  112. $response->expects($this->once())
  113. ->method('getBody')
  114. ->with()
  115. ->willReturn($body);
  116. $response->expects($this->once())
  117. ->method('getHeader')
  118. ->with('Content-Type')
  119. ->willReturn($contentType);
  120. $this->caldavBackend->expects($this->once())
  121. ->method('purgeAllCachedEventsForSubscription')
  122. ->with(42);
  123. $this->caldavBackend->expects($this->once())
  124. ->method('createCalendarObject')
  125. ->with(42, 'uri-1.ics', $result, 1);
  126. $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
  127. }
  128. /**
  129. * @param string $body
  130. * @param string $contentType
  131. * @param string $result
  132. *
  133. * @dataProvider runDataProvider
  134. */
  135. public function testRunCreateCalendarNoException(string $body, string $contentType, string $result): void {
  136. $client = $this->createMock(IClient::class);
  137. $response = $this->createMock(IResponse::class);
  138. $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
  139. ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription', 'queryWebcalFeed'])
  140. ->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
  141. ->getMock();
  142. $refreshWebcalService
  143. ->method('getRandomCalendarObjectUri')
  144. ->willReturn('uri-1.ics');
  145. $refreshWebcalService
  146. ->method('getSubscription')
  147. ->willReturn([
  148. 'id' => '42',
  149. 'uri' => 'sub123',
  150. '{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
  151. '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
  152. '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
  153. '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
  154. 'source' => 'webcal://foo.bar/bla2'
  155. ]);
  156. $this->clientService->expects($this->once())
  157. ->method('newClient')
  158. ->with()
  159. ->willReturn($client);
  160. $this->config->expects($this->once())
  161. ->method('getAppValue')
  162. ->with('dav', 'webcalAllowLocalAccess', 'no')
  163. ->willReturn('no');
  164. $client->expects($this->once())
  165. ->method('get')
  166. ->with('https://foo.bar/bla2', $this->callback(function ($obj) {
  167. return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
  168. }))
  169. ->willReturn($response);
  170. $response->expects($this->once())
  171. ->method('getBody')
  172. ->with()
  173. ->willReturn($body);
  174. $response->expects($this->once())
  175. ->method('getHeader')
  176. ->with('Content-Type')
  177. ->willReturn($contentType);
  178. $this->caldavBackend->expects($this->once())
  179. ->method('purgeAllCachedEventsForSubscription')
  180. ->with(42);
  181. $this->caldavBackend->expects($this->once())
  182. ->method('createCalendarObject')
  183. ->with(42, 'uri-1.ics', $result, 1);
  184. $noInstanceException = new NoInstancesException("can't add calendar object");
  185. $this->caldavBackend->expects($this->once())
  186. ->method("createCalendarObject")
  187. ->willThrowException($noInstanceException);
  188. $this->logger->expects($this->once())
  189. ->method('error')
  190. ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $noInstanceException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
  191. $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
  192. }
  193. /**
  194. * @param string $body
  195. * @param string $contentType
  196. * @param string $result
  197. *
  198. * @dataProvider runDataProvider
  199. */
  200. public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result): void {
  201. $client = $this->createMock(IClient::class);
  202. $response = $this->createMock(IResponse::class);
  203. $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
  204. ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription', 'queryWebcalFeed'])
  205. ->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
  206. ->getMock();
  207. $refreshWebcalService
  208. ->method('getRandomCalendarObjectUri')
  209. ->willReturn('uri-1.ics');
  210. $refreshWebcalService
  211. ->method('getSubscription')
  212. ->willReturn([
  213. 'id' => '42',
  214. 'uri' => 'sub123',
  215. '{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
  216. '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
  217. '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
  218. '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
  219. 'source' => 'webcal://foo.bar/bla2'
  220. ]);
  221. $this->clientService->expects($this->once())
  222. ->method('newClient')
  223. ->with()
  224. ->willReturn($client);
  225. $this->config->expects($this->once())
  226. ->method('getAppValue')
  227. ->with('dav', 'webcalAllowLocalAccess', 'no')
  228. ->willReturn('no');
  229. $client->expects($this->once())
  230. ->method('get')
  231. ->with('https://foo.bar/bla2', $this->callback(function ($obj) {
  232. return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
  233. }))
  234. ->willReturn($response);
  235. $response->expects($this->once())
  236. ->method('getBody')
  237. ->with()
  238. ->willReturn($body);
  239. $response->expects($this->once())
  240. ->method('getHeader')
  241. ->with('Content-Type')
  242. ->willReturn($contentType);
  243. $this->caldavBackend->expects($this->once())
  244. ->method('purgeAllCachedEventsForSubscription')
  245. ->with(42);
  246. $this->caldavBackend->expects($this->once())
  247. ->method('createCalendarObject')
  248. ->with(42, 'uri-1.ics', $result, 1);
  249. $badRequestException = new BadRequest("can't add reach calendar url");
  250. $this->caldavBackend->expects($this->once())
  251. ->method("createCalendarObject")
  252. ->willThrowException($badRequestException);
  253. $this->logger->expects($this->once())
  254. ->method('error')
  255. ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $badRequestException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
  256. $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
  257. }
  258. /**
  259. * @return array
  260. */
  261. public function runDataProvider():array {
  262. return [
  263. [
  264. "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
  265. 'text/calendar;charset=utf8',
  266. "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
  267. ],
  268. [
  269. '["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
  270. 'application/calendar+json',
  271. "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VTIMEZONE\r\nLAST-MODIFIED:20040110T032845Z\r\nTZID:US/Eastern\r\nBEGIN:DAYLIGHT\r\nDTSTART:20000404T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20001026T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060102T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
  272. ],
  273. [
  274. '<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
  275. 'application/calendar+xml',
  276. "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060104T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2 bis\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
  277. ]
  278. ];
  279. }
  280. /**
  281. * @dataProvider runLocalURLDataProvider
  282. */
  283. public function testRunLocalURL(string $source): void {
  284. $refreshWebcalService = new RefreshWebcalService(
  285. $this->caldavBackend,
  286. $this->clientService,
  287. $this->config,
  288. $this->logger
  289. );
  290. $this->caldavBackend->expects($this->once())
  291. ->method('getSubscriptionsForUser')
  292. ->with('principals/users/testuser')
  293. ->willReturn([
  294. [
  295. 'id' => 42,
  296. 'uri' => 'sub123',
  297. 'refreshreate' => 'P1H',
  298. 'striptodos' => 1,
  299. 'stripalarms' => 1,
  300. 'stripattachments' => 1,
  301. 'source' => $source
  302. ],
  303. ]);
  304. $client = $this->createMock(IClient::class);
  305. $this->clientService->expects($this->once())
  306. ->method('newClient')
  307. ->with()
  308. ->willReturn($client);
  309. $this->config->expects($this->once())
  310. ->method('getAppValue')
  311. ->with('dav', 'webcalAllowLocalAccess', 'no')
  312. ->willReturn('no');
  313. $localServerException = new LocalServerException();
  314. $client->expects($this->once())
  315. ->method('get')
  316. ->willThrowException($localServerException);
  317. $this->logger->expects($this->once())
  318. ->method('warning')
  319. ->with("Subscription 42 was not refreshed because it violates local access rules", ['exception' => $localServerException]);
  320. $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
  321. }
  322. public function runLocalURLDataProvider():array {
  323. return [
  324. ['localhost/foo.bar'],
  325. ['localHost/foo.bar'],
  326. ['random-host/foo.bar'],
  327. ['[::1]/bla.blub'],
  328. ['[::]/bla.blub'],
  329. ['192.168.0.1'],
  330. ['172.16.42.1'],
  331. ['[fdf8:f53b:82e4::53]/secret.ics'],
  332. ['[fe80::200:5aee:feaa:20a2]/secret.ics'],
  333. ['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
  334. ['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
  335. ['10.0.0.1'],
  336. ['another-host.local'],
  337. ['service.localhost'],
  338. ];
  339. }
  340. public function testInvalidUrl(): void {
  341. $refreshWebcalService = new RefreshWebcalService($this->caldavBackend,
  342. $this->clientService, $this->config, $this->logger);
  343. $this->caldavBackend->expects($this->once())
  344. ->method('getSubscriptionsForUser')
  345. ->with('principals/users/testuser')
  346. ->willReturn([
  347. [
  348. 'id' => 42,
  349. 'uri' => 'sub123',
  350. 'refreshreate' => 'P1H',
  351. 'striptodos' => 1,
  352. 'stripalarms' => 1,
  353. 'stripattachments' => 1,
  354. 'source' => '!@#$'
  355. ],
  356. ]);
  357. $client = $this->createMock(IClient::class);
  358. $this->clientService->expects($this->once())
  359. ->method('newClient')
  360. ->with()
  361. ->willReturn($client);
  362. $this->config->expects($this->once())
  363. ->method('getAppValue')
  364. ->with('dav', 'webcalAllowLocalAccess', 'no')
  365. ->willReturn('no');
  366. $client->expects($this->never())
  367. ->method('get');
  368. $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
  369. }
  370. }