Browse Source

Implement all TTRSS feed and category handling except subscribing to feeds

- Fixes #93
- Fixes #100
- Fixes #101
- Fixes #102
- Fixes #103
- Fixes #104
microsub
J. King 7 years ago
parent
commit
91cce6b529
  1. 133
      lib/REST/TinyTinyRSS/API.php
  2. 208
      tests/REST/TinyTinyRSS/TestTinyTinyAPI.php

133
lib/REST/TinyTinyRSS/API.php

@ -6,11 +6,24 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Feed\Exception as FeedException;
use JKingWeb\Arsse\REST\Response;
/*
Protocol difference so far:
- handling of incorrect Content-Type and/or HTTP method is different
- TT-RSS accepts whitespace-only names; we do not
- TT-RSS allows two folders to share the same name under the same parent; we do not
- Session lifetime is much shorter by default (does TT-RSS even expire sessions?)
*/
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
const LEVEL = 14;
const VERSION = "17.4";
@ -143,20 +156,126 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
return Arsse::$db->folderAdd(Arsse::$user->id, $in);
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
// folder already exists
case 10236:
// retrieve the ID of the existing folder; duplicating a category silently returns the existing one
case 10236: // folder already exists
// retrieve the ID of the existing folder; duplicating a folder silently returns the existing one
$folders = Arsse::$db->folderList(Arsse::$user->id, $in['parent'], false);
foreach ($folders as $folder) {
if ($folder['name']==$in['name']) {
return (int) $folder['id'];
}
}
// parent folder does not exist; this returns false as an ID
case 10235: return false;
// other errors related to input
default: throw new Exception("INCORRECT_USAGE");
return false;
case 10235: // parent folder does not exist; this returns false as an ID
return false;
default: // other errors related to input
throw new Exception("INCORRECT_USAGE");
}
}
}
public function opRemoveCategory(array $data) {
if (!isset($data['category_id']) || !ValueInfo::id($data['category_id'])) {
// if the folder is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
try {
// attempt to remove the folder
Arsse::$db->folderRemove(Arsse::$user->id, (int) $data['category_id']);
} catch(ExceptionInput $e) {
// ignore all errors
}
return null;
}
public function opMoveCategory(array $data) {
if (!isset($data['category_id']) || !ValueInfo::id($data['category_id']) || !isset($data['parent_id']) || !ValueInfo::id($data['parent_id'], true)) {
// if the folder or parent is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
$in = [
'parent' => (int) $data['parent_id'],
];
try {
// try to move the folder
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $data['category_id'], $in);
} catch(ExceptionInput $e) {
// ignore all errors
}
return null;
}
public function opRenameCategory(array $data) {
if (!isset($data['category_id']) || !ValueInfo::id($data['category_id']) || !isset($data['caption'])) {
// if the folder is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
$info = ValueInfo::str($data['caption']);
if (!($info & ValueInfo::VALID) || ($info & ValueInfo::EMPTY) || ($info & ValueInfo::WHITE)) {
// if the folder name is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
$in = [
'name' => (string) $data['caption'],
];
try {
// try to rename the folder
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $data['category_id'], $in);
} catch(ExceptionInput $e) {
// ignore all errors
}
return null;
}
public function opUnsubscribeFeed(array $data): array {
if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id'])) {
// if the feed is invalid, throw an error
throw new Exception("FEED_NOT_FOUND");
}
try {
// attempt to remove the feed
Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $data['feed_id']);
} catch(ExceptionInput $e) {
throw new Exception("FEED_NOT_FOUND");
}
return ['status' => "OK"];
}
public function opMoveFeed(array $data) {
if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id']) || !isset($data['category_id']) || !ValueInfo::id($data['category_id'], true)) {
// if the feed or folder is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
$in = [
'folder' => (int) $data['category_id'],
];
try {
// try to move the feed
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $data['feed_id'], $in);
} catch(ExceptionInput $e) {
// ignore all errors
}
return null;
}
public function opRenameFeed(array $data) {
if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id']) || !isset($data['caption'])) {
// if the feed is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
$info = ValueInfo::str($data['caption']);
if (!($info & ValueInfo::VALID) || ($info & ValueInfo::EMPTY) || ($info & ValueInfo::WHITE)) {
// if the feed name is invalid, throw an error
throw new Exception("INCORRECT_USAGE");
}
$in = [
'name' => (string) $data['caption'],
];
try {
// try to rename the feed
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $data['feed_id'], $in);
} catch(ExceptionInput $e) {
// ignore all errors
}
return null;
}
}

208
tests/REST/TinyTinyRSS/TestTinyTinyAPI.php

@ -293,4 +293,212 @@ class TestTinyTinyAPI extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
}
public function testRemoveACategory() {
$in = [
['op' => "removeCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42],
['op' => "removeCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 2112],
['op' => "removeCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => -1],
];
Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, 42)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
// succefully delete a folder
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// try deleting it again (this should silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// delete a folder which does not exist (this should also silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
// delete an invalid folder (causes an error)
$exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
Phake::verify(Arsse::$db, Phake::times(3))->folderRemove(Arsse::$user->id, $this->anything());
}
public function testMoveACategory() {
$in = [
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => 1],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 2112, 'parent_id' => 2],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => 0],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => 47],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => -1, 'parent_id' => 1],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => -1],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'parent_id' => -1],
['op' => "moveCategory", 'sid' => "PriestsOfSyrinx"],
];
$db = [
[Arsse::$user->id, 42, ['parent' => 1]],
[Arsse::$user->id, 2112, ['parent' => 2]],
[Arsse::$user->id, 42, ['parent' => 0]],
[Arsse::$user->id, 42, ['parent' => 47]],
];
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[0])->thenReturn(true);
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation"));
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("idMissing"));
// succefully move a folder
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// move a folder which does not exist (this should silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
// move a folder causing a duplication (this should also silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
// all the rest should cause errors
$exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[6]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[7]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[8]))));
Phake::verify(Arsse::$db, Phake::times(4))->folderPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
}
public function testRenameACategory() {
$in = [
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => "Ook"],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 2112, 'caption' => "Eek"],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => "Eek"],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => ""],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => " "],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => -1, 'caption' => "Ook"],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"],
['op' => "renameCategory", 'sid' => "PriestsOfSyrinx"],
];
$db = [
[Arsse::$user->id, 42, ['name' => "Ook"]],
[Arsse::$user->id, 2112, ['name' => "Eek"]],
[Arsse::$user->id, 42, ['name' => "Eek"]],
];
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[0])->thenReturn(true);
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation"));
// succefully rename a folder
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// rename a folder which does not exist (this should silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
// rename a folder causing a duplication (this should also silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
// all the rest should cause errors
$exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[6]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[7]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[8]))));
Phake::verify(Arsse::$db, Phake::times(3))->folderPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
}
public function testRemoveASubscription() {
$in = [
['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42],
['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112],
['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1],
['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx"],
];
Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 42)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
// succefully delete a folder
$exp = $this->respGood(['status' => "OK"]);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// try deleting it again (this should noisily fail, as should everything else)
$exp = $this->respErr("FEED_NOT_FOUND");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
Phake::verify(Arsse::$db, Phake::times(3))->subscriptionRemove(Arsse::$user->id, $this->anything());
}
public function testMoveASubscription() {
$in = [
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => 1],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112, 'category_id' => 2],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => 0],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => 47],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'category_id' => 1],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => -1],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'category_id' => -1],
['op' => "moveFeed", 'sid' => "PriestsOfSyrinx"],
];
$db = [
[Arsse::$user->id, 42, ['folder' => 1]],
[Arsse::$user->id, 2112, ['folder' => 2]],
[Arsse::$user->id, 42, ['folder' => 0]],
[Arsse::$user->id, 42, ['folder' => 47]],
];
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[0])->thenReturn(true);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation"));
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("constraintViolation"));
// succefully move a subscription
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// move a subscription which does not exist (this should silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
// move a subscription causing a duplication (this should also silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
// all the rest should cause errors
$exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[6]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[7]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[8]))));
Phake::verify(Arsse::$db, Phake::times(4))->subscriptionPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
}
public function testRenameASubscription() {
$in = [
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => "Ook"],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112, 'caption' => "Eek"],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => "Eek"],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => ""],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => " "],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'caption' => "Ook"],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"],
['op' => "renameFeed", 'sid' => "PriestsOfSyrinx"],
];
$db = [
[Arsse::$user->id, 42, ['name' => "Ook"]],
[Arsse::$user->id, 2112, ['name' => "Eek"]],
[Arsse::$user->id, 42, ['name' => "Eek"]],
];
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[0])->thenReturn(true);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation"));
// succefully rename a subscription
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
// rename a subscription which does not exist (this should silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
// rename a subscription causing a duplication (this should also silently fail)
$exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
// all the rest should cause errors
$exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[6]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[7]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[8]))));
Phake::verify(Arsse::$db, Phake::times(3))->subscriptionPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
}
}

Loading…
Cancel
Save