1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

Convert article and edition ranges to atomic

Unit tests for ranges are still missing
This commit is contained in:
J. King 2022-04-19 22:53:36 -04:00
parent 2c2bb4a856
commit 983fa58ec8
11 changed files with 85 additions and 67 deletions

View file

@ -1579,7 +1579,16 @@ class Database {
continue;
} elseif ($op === "between") {
// option is a range
$q->setWhereNot("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], $context->$m);
if ($context->$m[0] === null) {
// range is open at the low end
$q->setWhere("{$colDefs[$col]} <= ?", $type, $context->$m[1]);
} elseif ($context->$m[1] === null) {
// range is open at the high end
$q->setWhere("{$colDefs[$col]} >= ?", $type, $context->$m[0]);
} else {
// range is bounded in both directions
$q->setWhere("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], $context->$m);
}
} elseif (is_array($context->$m)) {
// context option is an array of values
if (!$context->$m) {
@ -1598,7 +1607,16 @@ class Database {
continue;
} elseif ($op === "between") {
// option is a range
if ($context->not->$m[0] === null) {
// range is open at the low end
$q->setWhereNot("{$colDefs[$col]} <= ?", $type, $context->not->$m[1]);
} elseif ($context->not->$m[1] === null) {
// range is open at the high end
$q->setWhereNot("{$colDefs[$col]} >= ?", $type, $context->not->$m[0]);
} else {
// range is bounded in both directions
$q->setWhereNot("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], $context->not->$m);
}
} elseif (is_array($context->not->$m)) {
if (!$context->not->$m) {
// for exclusions we don't care if the array is empty

View file

@ -388,10 +388,10 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
if ($G['with_ids']) {
$c->articles(explode(",", $G['with_ids']))->hidden(null);
} elseif ($G['max_id']) {
$c->latestArticle($G['max_id'] - 1);
$c->articleRange(null, $G['max_id'] - 1);
$reverse = true;
} elseif ($G['since_id']) {
$c->oldestArticle($G['since_id'] + 1);
$c->articleRange($G['since_id'] + 1, null);
}
// handle the undocumented options
if ($G['group_ids']) {

View file

@ -894,8 +894,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
->offset($query['offset'])
->starred($query['starred'])
->modifiedRange($query['after'], $query['before']) // FIXME: This may not be the correct date field
->oldestArticle($query['after_entry_id'] ? $query['after_entry_id'] + 1 : null) // FIXME: This might be edition
->latestArticle($query['before_entry_id'] ? $query['before_entry_id'] - 1 : null)
->articleRange($query['after_entry_id'] ? $query['after_entry_id'] + 1 : null, $query['before_entry_id'] ? $query['before_entry_id'] - 1 : null) // FIXME: This might be edition
->searchTerms(strlen($query['search'] ?? "") ? preg_split("/\s+/", $query['search']) : null); // NOTE: Miniflux matches only whole words; we match simple substrings
if ($query['category_id']) {
if ($query['category_id'] === 1) {

View file

@ -346,7 +346,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
// build the context
$c = (new Context)->hidden(false);
$c->latestEdition((int) $data['newestItemId']);
$c->editionRange(null, (int) $data['newestItemId']);
$c->folder((int) $url[1]);
// perform the operation
try {
@ -501,7 +501,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
// build the context
$c = (new Context)->hidden(false);
$c->latestEdition((int) $data['newestItemId']);
$c->editionRange(null, (int) $data['newestItemId']);
$c->subscription((int) $url[1]);
// perform the operation
try {
@ -526,9 +526,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
if ($data['offset'] > 0) {
if ($reverse) {
$c->latestEdition($data['offset'] - 1);
$c->editionRange(null, $data['offset'] - 1);
} else {
$c->oldestEdition($data['offset'] + 1);
$c->editionRange($data['offset'] + 1, null);
}
}
// set whether to only return unread
@ -597,7 +597,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
// build the context
$c = (new Context)->hidden(false);
$c->latestEdition((int) $data['newestItemId']);
$c->editionRange(null, (int) $data['newestItemId']);
// perform the operation
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
return new EmptyResponse(204);

View file

@ -1550,7 +1550,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
}
// set the minimum article ID
if ($data['since_id'] > 0) {
$c->oldestArticle($data['since_id'] + 1);
$c->articleRange($data['since_id'] + 1, null);
}
// return results
return Arsse::$db->articleList(Arsse::$user->id, $c, $fields, $order);

View file

@ -456,12 +456,12 @@ trait SeriesArticle {
'Not hidden' => [(new Context)->hidden(false), [1,2,3,4,5,7,8,19,20]],
'Labelled' => [(new Context)->labelled(true), [1,5,8,19,20]],
'Not labelled' => [(new Context)->labelled(false), [2,3,4,6,7]],
'Not after edition 999' => [(new Context)->subscription(5)->latestEdition(999), [19]],
'Not after edition 19' => [(new Context)->subscription(5)->latestEdition(19), [19]],
'Not before edition 999' => [(new Context)->subscription(5)->oldestEdition(999), [20]],
'Not before edition 1001' => [(new Context)->subscription(5)->oldestEdition(1001), [20]],
'Not after article 3' => [(new Context)->latestArticle(3), [1,2,3]],
'Not before article 19' => [(new Context)->oldestArticle(19), [19,20]],
'Not after edition 999' => [(new Context)->subscription(5)->editionRange(null, 999), [19]],
'Not after edition 19' => [(new Context)->subscription(5)->editionRange(null, 19), [19]],
'Not before edition 999' => [(new Context)->subscription(5)->editionRange(999, null), [20]],
'Not before edition 1001' => [(new Context)->subscription(5)->editionRange(1001, null), [20]],
'Not after article 3' => [(new Context)->articleRange(null, 3), [1,2,3]],
'Not before article 19' => [(new Context)->articleRange(19, null), [19,20]],
'Modified by author since 2005' => [(new Context)->modifiedRange("2005-01-01T00:00:00Z", null), [2,4,6,8,20]],
'Modified by author since 2010' => [(new Context)->modifiedRange("2010-01-01T00:00:00Z", null), [2,4,6,8,20]],
'Not modified by author since 2005' => [(new Context)->modifiedRange(null, "2005-01-01T00:00:00Z"), [1,3,5,7,19]],
@ -472,7 +472,7 @@ trait SeriesArticle {
'Not marked or labelled since 2005' => [(new Context)->markedRange(null, "2005-01-01T00:00:00Z"), [1,3,5,7]],
'Marked or labelled between 2000 and 2015' => [(new Context)->markedRange("2000-01-01T00:00:00Z", "2015-12-31T23:59:59Z"), [1,2,3,4,5,6,7,8,20]],
'Marked or labelled in 2010' => [(new Context)->markedRange("2010-01-01T00:00:00Z", "2010-12-31T23:59:59Z"), [2,4,6,20]],
'Paged results' => [(new Context)->limit(2)->oldestEdition(4), [4,5]],
'Paged results' => [(new Context)->limit(2)->editionRange(4, null), [4,5]],
'With label ID 1' => [(new Context)->label(1), [1,19]],
'With label ID 2' => [(new Context)->label(2), [1,5,20]],
'With label ID 1 or 2' => [(new Context)->labels([1,2]), [1,5,19,20]],
@ -929,7 +929,7 @@ trait SeriesArticle {
}
public function testMarkByOldestEdition(): void {
Arsse::$db->articleMark($this->user, ['starred' => true], (new Context)->oldestEdition(19));
Arsse::$db->articleMark($this->user, ['starred' => true], (new Context)->editionRange(19, null));
$now = Date::transform(time(), "sql");
$state = $this->primeExpectations($this->data, $this->checkTables);
$state['arsse_marks']['rows'][8][3] = 1;
@ -940,7 +940,7 @@ trait SeriesArticle {
}
public function testMarkByLatestEdition(): void {
Arsse::$db->articleMark($this->user, ['starred' => true], (new Context)->latestEdition(20));
Arsse::$db->articleMark($this->user, ['starred' => true], (new Context)->editionRange(null, 20));
$now = Date::transform(time(), "sql");
$state = $this->primeExpectations($this->data, $this->checkTables);
$state['arsse_marks']['rows'][8][3] = 1;

View file

@ -11,6 +11,8 @@ use JKingWeb\Arsse\Misc\ValueInfo;
/** @covers \JKingWeb\Arsse\Context\Context<extended> */
class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
protected $ranges = ['modifiedRange', 'markedRange', 'articleRange', 'editionRange'];
public function testVerifyInitialState(): void {
$c = new Context;
foreach ((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
@ -19,9 +21,13 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
}
$method = $m->name;
$this->assertFalse($c->$method(), "Context method $method did not initially return false");
if (in_array($method, $this->ranges)) {
$this->assertEquals([null, null], $c->$method, "Context property $method is not initially a two-member falsy array");
} else {
$this->assertEquals(null, $c->$method, "Context property $method is not initially falsy");
}
}
}
public function testSetContextOptions(): void {
$v = [
@ -40,10 +46,6 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
'subscriptions' => [44, 2112],
'article' => 255,
'edition' => 65535,
'latestArticle' => 47,
'oldestArticle' => 1337,
'latestEdition' => 47,
'oldestEdition' => 1337,
'unread' => true,
'starred' => true,
'hidden' => true,
@ -61,10 +63,9 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
'authorTerms' => ["foo", "bar"],
'not' => (new Context)->subscription(5),
];
$ranges = ['modifiedRange', 'markedRange', 'articleRange', 'editionRange'];
$c = new Context;
foreach ((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
if ($m->isStatic() || strpos($m->name, "__") === 0 || in_array($m->name, $ranges)) {
if ($m->isStatic() || strpos($m->name, "__") === 0 || in_array($m->name, $this->ranges)) {
continue;
}
$method = $m->name;

View file

@ -316,12 +316,12 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
["items&group_ids=1,2,3,4", (clone $c)->tags([1,2,3,4])->hidden(false), false],
["items&feed_ids=1,2,3,4", (clone $c)->subscriptions([1,2,3,4])->hidden(false), false],
["items&with_ids=1,2,3,4", (clone $c)->articles([1,2,3,4]), false],
["items&since_id=1", (clone $c)->oldestArticle(2)->hidden(false), false],
["items&max_id=2", (clone $c)->latestArticle(1)->hidden(false), true],
["items&since_id=1", (clone $c)->articleRange(2, null)->hidden(false), false],
["items&max_id=2", (clone $c)->articleRange(null, 1)->hidden(false), true],
["items&with_ids=1,2,3,4&max_id=6", (clone $c)->articles([1,2,3,4]), false],
["items&with_ids=1,2,3,4&since_id=6", (clone $c)->articles([1,2,3,4]), false],
["items&max_id=3&since_id=6", (clone $c)->latestArticle(2)->hidden(false), true],
["items&feed_ids=1,2,3,4&since_id=6", (clone $c)->subscriptions([1,2,3,4])->oldestArticle(7)->hidden(false), false],
["items&max_id=3&since_id=6", (clone $c)->articleRange(null, 2)->hidden(false), true],
["items&feed_ids=1,2,3,4&since_id=6", (clone $c)->subscriptions([1,2,3,4])->articleRange(7, null)->hidden(false), false],
];
}

View file

@ -772,8 +772,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
["/entries?before=0", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=1", (clone $c)->modifiedRange(null, 1), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=1&after=0", (clone $c)->modifiedRange(0, 1), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?after_entry_id=42", (clone $c)->oldestArticle(43), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before_entry_id=47", (clone $c)->latestArticle(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?after_entry_id=42", (clone $c)->articleRange(43, null), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before_entry_id=47", (clone $c)->articleRange(null, 46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?search=alpha%20beta", (clone $c)->searchTerms(["alpha", "beta"]), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?limit=4", (clone $c)->limit(4), $o, self::ENTRIES, true, new Response(['total' => 2112, 'entries' => self::ENTRIES_OUT])],
["/entries?offset=20", (clone $c)->offset(20), $o, [], true, new Response(['total' => 2112, 'entries' => []])],

View file

@ -696,8 +696,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
["/items", ['getRead' => true], clone $c, $out, $r200],
["/items", ['getRead' => false], (clone $c)->unread(true), $out, $r200],
["/items", ['lastModified' => $t->getTimestamp()], (clone $c)->markedRange($t, null), $out, $r200],
["/items", ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], (clone $c)->oldestEdition(6)->limit(10), $out, $r200],
["/items", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], (clone $c)->latestEdition(4)->limit(5), $out, $r200],
["/items", ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], (clone $c)->editionRange(6, null)->limit(10), $out, $r200],
["/items", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], (clone $c)->editionRange(null, 4)->limit(5), $out, $r200],
["/items", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], (clone $c)->limit(5), $out, $r200],
["/items/updated", [], clone $c, $out, $r200],
["/items/updated", ['type' => 0, 'id' => 42], (clone $c)->subscription(42), new ExceptionInput("idMissing"), $r422],
@ -709,8 +709,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
["/items/updated", ['getRead' => true], clone $c, $out, $r200],
["/items/updated", ['getRead' => false], (clone $c)->unread(true), $out, $r200],
["/items/updated", ['lastModified' => $t->getTimestamp()], (clone $c)->markedRange($t, null), $out, $r200],
["/items/updated", ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], (clone $c)->oldestEdition(6)->limit(10), $out, $r200],
["/items/updated", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], (clone $c)->latestEdition(4)->limit(5), $out, $r200],
["/items/updated", ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], (clone $c)->editionRange(6, null)->limit(10), $out, $r200],
["/items/updated", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], (clone $c)->editionRange(null, 4)->limit(5), $out, $r200],
["/items/updated", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], (clone $c)->limit(5), $out, $r200],
];
}
@ -718,8 +718,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function testMarkAFolderRead(): void {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->folder(1)->latestEdition(2112)->hidden(false)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->folder(42)->latestEdition(2112)->hidden(false)))->throws(new ExceptionInput("idMissing")); // folder doesn't exist
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->folder(1)->editionRange(null, 2112)->hidden(false)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->folder(42)->editionRange(null, 2112)->hidden(false)))->throws(new ExceptionInput("idMissing")); // folder doesn't exist
$exp = new EmptyResponse(204);
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read", $in));
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read?newestItemId=2112"));
@ -733,8 +733,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function testMarkASubscriptionRead(): void {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->subscription(1)->latestEdition(2112)->hidden(false)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->subscription(42)->latestEdition(2112)->hidden(false)))->throws(new ExceptionInput("idMissing")); // subscription doesn't exist
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->subscription(1)->editionRange(null, 2112)->hidden(false)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->subscription(42)->editionRange(null, 2112)->hidden(false)))->throws(new ExceptionInput("idMissing")); // subscription doesn't exist
$exp = new EmptyResponse(204);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read", $in));
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read?newestItemId=2112"));
@ -748,7 +748,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function testMarkAllItemsRead(): void {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->latestEdition(2112)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->editionRange(null, 2112)))->returns(42);
$exp = new EmptyResponse(204);
$this->assertMessage($exp, $this->req("PUT", "/items/read", $in));
$this->assertMessage($exp, $this->req("PUT", "/items/read?newestItemId=2112"));

View file

@ -1539,7 +1539,7 @@ LONG_STRING;
[true, ['feed_id' => -4, 'limit' => 5], $out, (clone $c)->limit(5), $fields, $sort, $expFull],
[true, ['feed_id' => -4, 'skip' => 2], $out, (clone $c)->offset(2), $fields, $sort, $expFull],
[true, ['feed_id' => -4, 'limit' => 5, 'skip' => 2], $out, (clone $c)->limit(5)->offset(2), $fields, $sort, $expFull],
[true, ['feed_id' => -4, 'since_id' => 47], $out, (clone $c)->oldestArticle(48), $fields, $sort, $expFull],
[true, ['feed_id' => -4, 'since_id' => 47], $out, (clone $c)->articleRange(48, null), $fields, $sort, $expFull],
[true, ['feed_id' => -3, 'is_cat' => true], $out, $c, $fields, $sort, $expFull],
[true, ['feed_id' => -4, 'is_cat' => true], $out, $c, $fields, $sort, $expFull],
[true, ['feed_id' => -2, 'is_cat' => true], $out, (clone $c)->labelled(true), $fields, $sort, $expFull],
@ -1571,7 +1571,7 @@ LONG_STRING;
[false, ['feed_id' => -4, 'limit' => 5], $comp, (clone $c)->limit(5), ["id"], $sort, $expComp],
[false, ['feed_id' => -4, 'skip' => 2], $comp, (clone $c)->limit(null)->offset(2), ["id"], $sort, $expComp],
[false, ['feed_id' => -4, 'limit' => 5, 'skip' => 2], $comp, (clone $c)->limit(5)->offset(2), ["id"], $sort, $expComp],
[false, ['feed_id' => -4, 'since_id' => 47], $comp, (clone $c)->limit(null)->oldestArticle(48), ["id"], $sort, $expComp],
[false, ['feed_id' => -4, 'since_id' => 47], $comp, (clone $c)->limit(null)->articleRange(48, null), ["id"], $sort, $expComp],
[false, ['feed_id' => -6], $comp, (clone $c)->limit(null)->unread(false)->markedRange(Date::sub("PT24H", $t), null), ["id"], ["marked_date desc"], $expComp],
[false, ['feed_id' => -6, 'view_mode' => "unread"], null, (clone $c)->limit(null), ["id"], $sort, $this->respGood([])],
[false, ['feed_id' => -3], $comp, (clone $c)->limit(null)->unread(true)->modifiedRange(Date::sub("PT24H", $t), null), ["id"], $sort, $expComp],