The clean & modern RSS server that doesn't give you any crap. https://thearsse.com/
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.

SeriesSubscription.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <?php
  2. /** @license MIT
  3. * Copyright 2017 J. King, Dustin Wilson et al.
  4. * See LICENSE and AUTHORS files for details */
  5. declare(strict_types=1);
  6. namespace JKingWeb\Arsse\TestCase\Database;
  7. use JKingWeb\Arsse\Arsse;
  8. use JKingWeb\Arsse\Test\Database;
  9. use JKingWeb\Arsse\Feed\Exception as FeedException;
  10. use Phake;
  11. trait SeriesSubscription {
  12. public function setUpSeriesSubscription() {
  13. $this->data = [
  14. 'arsse_users' => [
  15. 'columns' => [
  16. 'id' => 'str',
  17. 'password' => 'str',
  18. 'name' => 'str',
  19. ],
  20. 'rows' => [
  21. ["jane.doe@example.com", "", "Jane Doe"],
  22. ["john.doe@example.com", "", "John Doe"],
  23. ],
  24. ],
  25. 'arsse_folders' => [
  26. 'columns' => [
  27. 'id' => "int",
  28. 'owner' => "str",
  29. 'parent' => "int",
  30. 'name' => "str",
  31. ],
  32. 'rows' => [
  33. [1, "john.doe@example.com", null, "Technology"],
  34. [2, "john.doe@example.com", 1, "Software"],
  35. [3, "john.doe@example.com", 1, "Rocketry"],
  36. [4, "jane.doe@example.com", null, "Politics"],
  37. [5, "john.doe@example.com", null, "Politics"],
  38. [6, "john.doe@example.com", 2, "Politics"],
  39. ]
  40. ],
  41. 'arsse_feeds' => [
  42. 'columns' => [
  43. 'id' => "int",
  44. 'url' => "str",
  45. 'title' => "str",
  46. 'username' => "str",
  47. 'password' => "str",
  48. 'next_fetch' => "datetime",
  49. 'favicon' => "str",
  50. ],
  51. 'rows' => [] // filled in the series setup
  52. ],
  53. 'arsse_subscriptions' => [
  54. 'columns' => [
  55. 'id' => "int",
  56. 'owner' => "str",
  57. 'feed' => "int",
  58. 'title' => "str",
  59. 'folder' => "int",
  60. 'pinned' => "bool",
  61. 'order_type' => "int",
  62. ],
  63. 'rows' => [
  64. [1,"john.doe@example.com",2,null,null,1,2],
  65. [2,"jane.doe@example.com",2,null,null,0,0],
  66. [3,"john.doe@example.com",3,"Ook",2,0,1],
  67. ]
  68. ],
  69. 'arsse_articles' => [
  70. 'columns' => [
  71. 'id' => "int",
  72. 'feed' => "int",
  73. 'url_title_hash' => "str",
  74. 'url_content_hash' => "str",
  75. 'title_content_hash' => "str",
  76. ],
  77. 'rows' => [
  78. [1,2,"","",""],
  79. [2,2,"","",""],
  80. [3,2,"","",""],
  81. [4,2,"","",""],
  82. [5,2,"","",""],
  83. [6,3,"","",""],
  84. [7,3,"","",""],
  85. [8,3,"","",""],
  86. ]
  87. ],
  88. 'arsse_marks' => [
  89. 'columns' => [
  90. 'article' => "int",
  91. 'subscription' => "int",
  92. 'read' => "bool",
  93. 'starred' => "bool",
  94. ],
  95. 'rows' => [
  96. [1,2,1,0],
  97. [2,2,1,0],
  98. [3,2,1,0],
  99. [4,2,1,0],
  100. [5,2,1,0],
  101. [1,1,1,0],
  102. [7,3,1,0],
  103. [8,3,0,0],
  104. ]
  105. ],
  106. ];
  107. $this->data['arsse_feeds']['rows'] = [
  108. [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),''],
  109. [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),'http://example.com/favicon.ico'],
  110. [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),''],
  111. ];
  112. // initialize a partial mock of the Database object to later manipulate the feedUpdate method
  113. Arsse::$db = Phake::partialMock(Database::class, static::$drv);
  114. $this->user = "john.doe@example.com";
  115. }
  116. protected function tearDownSeriesSubscription() {
  117. unset($this->data, $this->user);
  118. }
  119. public function testAddASubscriptionToAnExistingFeed() {
  120. $url = "http://example.com/feed1";
  121. $subID = $this->nextID("arsse_subscriptions");
  122. Phake::when(Arsse::$db)->feedUpdate->thenReturn(true);
  123. $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url));
  124. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd");
  125. Phake::verify(Arsse::$db, Phake::times(0))->feedUpdate(1, true);
  126. $state = $this->primeExpectations($this->data, [
  127. 'arsse_feeds' => ['id','url','username','password'],
  128. 'arsse_subscriptions' => ['id','owner','feed'],
  129. ]);
  130. $state['arsse_subscriptions']['rows'][] = [$subID,$this->user,1];
  131. $this->compareExpectations($state);
  132. }
  133. public function testAddASubscriptionToANewFeed() {
  134. $url = "http://example.org/feed1";
  135. $feedID = $this->nextID("arsse_feeds");
  136. $subID = $this->nextID("arsse_subscriptions");
  137. Phake::when(Arsse::$db)->feedUpdate->thenReturn(true);
  138. $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", false));
  139. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd");
  140. Phake::verify(Arsse::$db)->feedUpdate($feedID, true);
  141. $state = $this->primeExpectations($this->data, [
  142. 'arsse_feeds' => ['id','url','username','password'],
  143. 'arsse_subscriptions' => ['id','owner','feed'],
  144. ]);
  145. $state['arsse_feeds']['rows'][] = [$feedID,$url,"",""];
  146. $state['arsse_subscriptions']['rows'][] = [$subID,$this->user,$feedID];
  147. $this->compareExpectations($state);
  148. }
  149. public function testAddASubscriptionToANewFeedViaDiscovery() {
  150. $url = "http://localhost:8000/Feed/Discovery/Valid";
  151. $discovered = "http://localhost:8000/Feed/Discovery/Feed";
  152. $feedID = $this->nextID("arsse_feeds");
  153. $subID = $this->nextID("arsse_subscriptions");
  154. Phake::when(Arsse::$db)->feedUpdate->thenReturn(true);
  155. $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", true));
  156. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd");
  157. Phake::verify(Arsse::$db)->feedUpdate($feedID, true);
  158. $state = $this->primeExpectations($this->data, [
  159. 'arsse_feeds' => ['id','url','username','password'],
  160. 'arsse_subscriptions' => ['id','owner','feed'],
  161. ]);
  162. $state['arsse_feeds']['rows'][] = [$feedID,$discovered,"",""];
  163. $state['arsse_subscriptions']['rows'][] = [$subID,$this->user,$feedID];
  164. $this->compareExpectations($state);
  165. }
  166. public function testAddASubscriptionToAnInvalidFeed() {
  167. $url = "http://example.org/feed1";
  168. $feedID = $this->nextID("arsse_feeds");
  169. Phake::when(Arsse::$db)->feedUpdate->thenThrow(new FeedException($url, new \PicoFeed\Client\InvalidUrlException()));
  170. try {
  171. Arsse::$db->subscriptionAdd($this->user, $url, "", "", false);
  172. } catch (FeedException $e) {
  173. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd");
  174. Phake::verify(Arsse::$db)->feedUpdate($feedID, true);
  175. $state = $this->primeExpectations($this->data, [
  176. 'arsse_feeds' => ['id','url','username','password'],
  177. 'arsse_subscriptions' => ['id','owner','feed'],
  178. ]);
  179. $this->compareExpectations($state);
  180. $this->assertException("invalidUrl", "Feed");
  181. throw $e;
  182. }
  183. }
  184. public function testAddADuplicateSubscription() {
  185. $url = "http://example.com/feed2";
  186. $this->assertException("constraintViolation", "Db", "ExceptionInput");
  187. Arsse::$db->subscriptionAdd($this->user, $url);
  188. }
  189. public function testAddASubscriptionWithoutAuthority() {
  190. $url = "http://example.com/feed1";
  191. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  192. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  193. Arsse::$db->subscriptionAdd($this->user, $url);
  194. }
  195. public function testRemoveASubscription() {
  196. $this->assertTrue(Arsse::$db->subscriptionRemove($this->user, 1));
  197. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionRemove");
  198. $state = $this->primeExpectations($this->data, [
  199. 'arsse_feeds' => ['id','url','username','password'],
  200. 'arsse_subscriptions' => ['id','owner','feed'],
  201. ]);
  202. array_shift($state['arsse_subscriptions']['rows']);
  203. $this->compareExpectations($state);
  204. }
  205. public function testRemoveAMissingSubscription() {
  206. $this->assertException("subjectMissing", "Db", "ExceptionInput");
  207. Arsse::$db->subscriptionRemove($this->user, 2112);
  208. }
  209. public function testRemoveAnInvalidSubscription() {
  210. $this->assertException("typeViolation", "Db", "ExceptionInput");
  211. Arsse::$db->subscriptionRemove($this->user, -1);
  212. }
  213. public function testRemoveASubscriptionForTheWrongOwner() {
  214. $this->user = "jane.doe@example.com";
  215. $this->assertException("subjectMissing", "Db", "ExceptionInput");
  216. Arsse::$db->subscriptionRemove($this->user, 1);
  217. }
  218. public function testRemoveASubscriptionWithoutAuthority() {
  219. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  220. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  221. Arsse::$db->subscriptionRemove($this->user, 1);
  222. }
  223. public function testListSubscriptions() {
  224. $exp = [
  225. [
  226. 'url' => "http://example.com/feed2",
  227. 'title' => "eek",
  228. 'folder' => null,
  229. 'top_folder' => null,
  230. 'unread' => 4,
  231. 'pinned' => 1,
  232. 'order_type' => 2,
  233. ],
  234. [
  235. 'url' => "http://example.com/feed3",
  236. 'title' => "Ook",
  237. 'folder' => 2,
  238. 'top_folder' => 1,
  239. 'unread' => 2,
  240. 'pinned' => 0,
  241. 'order_type' => 1,
  242. ],
  243. ];
  244. $this->assertResult($exp, Arsse::$db->subscriptionList($this->user));
  245. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionList");
  246. $this->assertArraySubset($exp[0], Arsse::$db->subscriptionPropertiesGet($this->user, 1));
  247. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionPropertiesGet");
  248. $this->assertArraySubset($exp[1], Arsse::$db->subscriptionPropertiesGet($this->user, 3));
  249. }
  250. public function testListSubscriptionsInAFolder() {
  251. $exp = [
  252. [
  253. 'url' => "http://example.com/feed2",
  254. 'title' => "eek",
  255. 'folder' => null,
  256. 'top_folder' => null,
  257. 'unread' => 4,
  258. 'pinned' => 1,
  259. 'order_type' => 2,
  260. ],
  261. ];
  262. $this->assertResult($exp, Arsse::$db->subscriptionList($this->user, null, false));
  263. }
  264. public function testListSubscriptionsWithoutRecursion() {
  265. $exp = [
  266. [
  267. 'url' => "http://example.com/feed3",
  268. 'title' => "Ook",
  269. 'folder' => 2,
  270. 'top_folder' => 1,
  271. 'unread' => 2,
  272. 'pinned' => 0,
  273. 'order_type' => 1,
  274. ],
  275. ];
  276. $this->assertResult($exp, Arsse::$db->subscriptionList($this->user, 2));
  277. }
  278. public function testListSubscriptionsInAMissingFolder() {
  279. $this->assertException("idMissing", "Db", "ExceptionInput");
  280. Arsse::$db->subscriptionList($this->user, 4);
  281. }
  282. public function testListSubscriptionsWithoutAuthority() {
  283. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  284. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  285. Arsse::$db->subscriptionList($this->user);
  286. }
  287. public function testCountSubscriptions() {
  288. $this->assertSame(2, Arsse::$db->subscriptionCount($this->user));
  289. $this->assertSame(1, Arsse::$db->subscriptionCount($this->user, 2));
  290. }
  291. public function testCountSubscriptionsInAMissingFolder() {
  292. $this->assertException("idMissing", "Db", "ExceptionInput");
  293. Arsse::$db->subscriptionCount($this->user, 4);
  294. }
  295. public function testCountSubscriptionsWithoutAuthority() {
  296. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  297. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  298. Arsse::$db->subscriptionCount($this->user);
  299. }
  300. public function testGetThePropertiesOfAMissingSubscription() {
  301. $this->assertException("subjectMissing", "Db", "ExceptionInput");
  302. Arsse::$db->subscriptionPropertiesGet($this->user, 2112);
  303. }
  304. public function testGetThePropertiesOfAnInvalidSubscription() {
  305. $this->assertException("typeViolation", "Db", "ExceptionInput");
  306. Arsse::$db->subscriptionPropertiesGet($this->user, -1);
  307. }
  308. public function testGetThePropertiesOfASubscriptionWithoutAuthority() {
  309. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  310. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  311. Arsse::$db->subscriptionPropertiesGet($this->user, 1);
  312. }
  313. public function testSetThePropertiesOfASubscription() {
  314. Arsse::$db->subscriptionPropertiesSet($this->user, 1, [
  315. 'title' => "Ook Ook",
  316. 'folder' => 3,
  317. 'pinned' => false,
  318. 'order_type' => 0,
  319. ]);
  320. Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionPropertiesSet");
  321. $state = $this->primeExpectations($this->data, [
  322. 'arsse_feeds' => ['id','url','username','password','title'],
  323. 'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type'],
  324. ]);
  325. $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,"Ook Ook",3,0,0];
  326. $this->compareExpectations($state);
  327. Arsse::$db->subscriptionPropertiesSet($this->user, 1, [
  328. 'title' => null,
  329. ]);
  330. $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0];
  331. $this->compareExpectations($state);
  332. // making no changes is a valid result
  333. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['unhinged' => true]);
  334. $this->compareExpectations($state);
  335. }
  336. public function testMoveASubscriptionToAMissingFolder() {
  337. $this->assertException("idMissing", "Db", "ExceptionInput");
  338. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['folder' => 4]);
  339. }
  340. public function testMoveASubscriptionToTheRootFolder() {
  341. $this->assertTrue(Arsse::$db->subscriptionPropertiesSet($this->user, 3, ['folder' => null]));
  342. }
  343. public function testRenameASubscriptionToABlankTitle() {
  344. $this->assertException("missing", "Db", "ExceptionInput");
  345. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => ""]);
  346. }
  347. public function testRenameASubscriptionToAWhitespaceTitle() {
  348. $this->assertException("whitespace", "Db", "ExceptionInput");
  349. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => " "]);
  350. }
  351. public function testRenameASubscriptionToFalse() {
  352. $this->assertException("typeViolation", "Db", "ExceptionInput");
  353. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => false]);
  354. }
  355. public function testRenameASubscriptionToZero() {
  356. $this->assertTrue(Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => 0]));
  357. }
  358. public function testRenameASubscriptionToAnArray() {
  359. $this->assertException("typeViolation", "Db", "ExceptionInput");
  360. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => []]);
  361. }
  362. public function testSetThePropertiesOfAMissingSubscription() {
  363. $this->assertException("subjectMissing", "Db", "ExceptionInput");
  364. Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]);
  365. }
  366. public function testSetThePropertiesOfAnInvalidSubscription() {
  367. $this->assertException("typeViolation", "Db", "ExceptionInput");
  368. Arsse::$db->subscriptionPropertiesSet($this->user, -1, ['folder' => null]);
  369. }
  370. public function testSetThePropertiesOfASubscriptionWithoutAuthority() {
  371. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  372. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  373. Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['folder' => null]);
  374. }
  375. public function testRetrieveTheFaviconOfASubscription() {
  376. $exp = "http://example.com/favicon.ico";
  377. $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1));
  378. $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2));
  379. $this->assertSame('', Arsse::$db->subscriptionFavicon(3));
  380. $this->assertSame('', Arsse::$db->subscriptionFavicon(4));
  381. // authorization shouldn't have any bearing on this function
  382. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  383. $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1));
  384. $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2));
  385. $this->assertSame('', Arsse::$db->subscriptionFavicon(3));
  386. $this->assertSame('', Arsse::$db->subscriptionFavicon(4));
  387. // invalid IDs should simply return an empty string
  388. $this->assertSame('', Arsse::$db->subscriptionFavicon(-2112));
  389. }
  390. public function testRetrieveTheFaviconOfASubscriptionWithUser() {
  391. $exp = "http://example.com/favicon.ico";
  392. $user = "john.doe@example.com";
  393. $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1, $user));
  394. $this->assertSame('', Arsse::$db->subscriptionFavicon(2, $user));
  395. $this->assertSame('', Arsse::$db->subscriptionFavicon(3, $user));
  396. $this->assertSame('', Arsse::$db->subscriptionFavicon(4, $user));
  397. $user = "jane.doe@example.com";
  398. $this->assertSame('', Arsse::$db->subscriptionFavicon(1, $user));
  399. $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2, $user));
  400. $this->assertSame('', Arsse::$db->subscriptionFavicon(3, $user));
  401. $this->assertSame('', Arsse::$db->subscriptionFavicon(4, $user));
  402. }
  403. public function testRetrieveTheFaviconOfASubscriptionWithUserWithoutAuthority() {
  404. $exp = "http://example.com/favicon.ico";
  405. $user = "john.doe@example.com";
  406. Phake::when(Arsse::$user)->authorize->thenReturn(false);
  407. $this->assertException("notAuthorized", "User", "ExceptionAuthz");
  408. Arsse::$db->subscriptionFavicon(-2112, $user);
  409. }
  410. }