Compare commits

...

68 Commits

Author SHA1 Message Date
J. King 0eb0478195 Merge branch 'master' into redup 4 months ago
J. King bbdc4f7672 Fix MySQL failure and shore up coverage 1 year ago
J. King eed42ddf19 Fix remaining tests 1 year ago
J. King 16af57cf90 Partially working cleanup tests 1 year ago
J. King c139f52ebc Start on rewriting cleanup tests 1 year ago
J. King 02301b0dd9 Rewrite article cleanup and update docs 1 year ago
J. King fa5786a4cc Fix up the simpler cleanup routines 1 year ago
J. King 56d733429d Tag-related sub soft-delete fixes 1 year ago
J. King fe1ef3489a Start on tag fixes 1 year ago
J. King de6760d5d7 More sub soft-delete fixes 1 year ago
J. King d1876773e8 Address more bugs with deleted subs 1 year ago
J. King 9c83b7ec18 Address some deficiencies in handling of deleted subscriptions 1 year ago
J. King 4762084102 Fix rest of import tests 1 year ago
J. King 91ac7b568b Mostly fix up import/export tests 1 year ago
J. King 63454b94d9 Fix label tests 1 year ago
J. King 6d9a3fb3bd Fix last article tests 1 year ago
J. King 7ddea9877e Fix most remaining article tests 1 year ago
J. King 04d26fc911 Properly revert edition querying 1 year ago
J. King 770e8fc98d A few more fixed article tests 1 year ago
J. King 4b3cfba495 Abandon the new marking method 1 year ago
J. King 4d0ce01acb Select editions properly 1 year ago
J. King 9fb57defa2 Sundry article test fixes; things are still broken 1 year ago
J. King 30dbd850e3 Remove references to marks table in label routines 1 year ago
J. King 212d842e05 Rewrite article marking procedure 1 year ago
J. King b1d2611e5b Fix up main article selection test series 1 year ago
J. King 19da22e144 Initial work on refactoring article tests 1 year ago
J. King b81596a2de Fix feed test series 1 year ago
J. King e110dfcf89 Partially fix up feed tests 1 year ago
J. King 9196dcfbc4 Remove the last uses of feedAdd 1 year ago
J. King 9d391469ad Merge branch 'master' into redup 1 year ago
J. King 2868f734e5 Merge branch 'master' into redup 1 year ago
J. King 5129ed710b Small fixe-ups 1 year ago
J. King ac38659e3a Fix most references to feedUpdate 1 year ago
J. King 7414d3844e Fix up the rest of the subscriptionUpdate function 1 year ago
J. King cc2f3ea996 Start on rewrite of feed updating 2 years ago
J. King 6958c24be2 Fix most subscription tests 2 years ago
J. King 9f784251e8 Fix up the aadding of subscription 2 years ago
J. King 2c9daedb14 Add provision for soft deletion of subscriptions 2 years ago
J. King b24a76b744 Fix up the simpler database functions 2 years ago
J. King 15a2e7fe0f Actually-last tests for schema upgrade 2 years ago
J. King 95d20f33c7 Last tests for schema upgrade 2 years ago
J. King 63e780b06d Tests for articles, with fixes 2 years ago
J. King 48accdfad8 Fix subs in the new MySQL schema 2 years ago
J. King df185bbe42 Address the schema changing on the service 2 years ago
J. King d5652296ea Test feed reduplication 2 years ago
J. King 307ab7fa2a Merge branch 'dbtest' into redup 2 years ago
J. King a77b47cd59 Merge branch 'master' into dbtest 2 years ago
J. King 387de940ff Start on update test 2 years ago
J. King 3f7df467e6 Tweak 2 years ago
J. King fc2428713a Fix remaining MySQL schema problems 2 years ago
J. King e4a7e6622b Fix most problems with the new schema 2 years ago
J. King 9459ef044f Use existing infrastructure for update tests 2 years ago
J. King fbf7848c14 Merge branch 'dbtest' into redup 2 years ago
J. King e9c6ddcfdf Adjust date fix-up after column changes 2 years ago
J. King 07bac4ead3 Remove colukmn types from test data 2 years ago
J. King 0f2da754c5 Fix remaining test problems 2 years ago
J. King c40f39e34e Work around MySQL absurdities 2 years ago
J. King 2822864a85 Fix most test failures 2 years ago
J. King 51ce4ae92b Partial rewrite of database table comparison 2 years ago
J. King 9ac615e4a4 Apply more PSR-12 style rules 2 years ago
J. King 4ed650fd87 Style fixes 2 years ago
J. King 2c19aa06b7 Put column defs in one place in tests 2 years ago
J. King bd728e3e12 New schema for MySQL 2 years ago
J. King 55012255bb New schema for PostgreSQL 2 years ago
J. King a2115a50fa Complete new database schema for SQLite 2 years ago
J. King 5a78fc0492 New schema fixup 2 years ago
J. King 94b816ff53 Fill out the new schema a bit more 2 years ago
J. King 3e2fce3129 Law out the plan for the new schema 2 years ago
  1. 13
      .php-cs-fixer.dist.php
  2. 2
      UPGRADING
  3. 12
      docs/en/020_Getting_Started/050_Configuration.md
  4. 1
      lib/AbstractException.php
  5. 2
      lib/CLI.php
  6. 4
      lib/Conf.php
  7. 642
      lib/Database.php
  8. 4
      lib/Db/AbstractDriver.php
  9. 4
      lib/Db/Driver.php
  10. 6
      lib/Db/MySQL/Driver.php
  11. 2
      lib/Db/MySQL/PDODriver.php
  12. 4
      lib/Db/PostgreSQL/Driver.php
  13. 4
      lib/Db/PostgreSQL/PDODriver.php
  14. 4
      lib/Db/SQLite3/PDODriver.php
  15. 18
      lib/Feed.php
  16. 44
      lib/ImportExport/AbstractImportExport.php
  17. 4
      lib/REST.php
  18. 17
      lib/REST/Miniflux/V1.php
  19. 2
      lib/REST/NextcloudNews/V1_2.php
  20. 2
      lib/REST/TinyTinyRSS/API.php
  21. 11
      lib/Service.php
  22. 2
      lib/Service/Serial/Driver.php
  23. 1
      locale/en.php
  24. 2
      sql/MySQL/0.sql
  25. 184
      sql/MySQL/7.sql
  26. 176
      sql/PostgreSQL/7.sql
  27. 203
      sql/SQLite3/7.sql
  28. 6
      tests/cases/CLI/TestCLI.php
  29. 795
      tests/cases/Database/SeriesArticle.php
  30. 196
      tests/cases/Database/SeriesCleanup.php
  31. 303
      tests/cases/Database/SeriesFeed.php
  32. 71
      tests/cases/Database/SeriesFolder.php
  33. 63
      tests/cases/Database/SeriesIcon.php
  34. 363
      tests/cases/Database/SeriesLabel.php
  35. 13
      tests/cases/Database/SeriesMeta.php
  36. 2
      tests/cases/Database/SeriesMiscellany.php
  37. 19
      tests/cases/Database/SeriesSession.php
  38. 412
      tests/cases/Database/SeriesSubscription.php
  39. 116
      tests/cases/Database/SeriesTag.php
  40. 20
      tests/cases/Database/SeriesToken.php
  41. 19
      tests/cases/Database/SeriesUser.php
  42. 3
      tests/cases/Database/TestDatabase.php
  43. 2
      tests/cases/Db/BaseDriver.php
  44. 235
      tests/cases/Db/BaseUpdate.php
  45. 2
      tests/cases/Exception/TestException.php
  46. 8
      tests/cases/Feed/TestException.php
  47. 23
      tests/cases/Feed/TestFeed.php
  48. 86
      tests/cases/ImportExport/TestImportExport.php
  49. 4
      tests/cases/Misc/TestContext.php
  50. 1
      tests/cases/Misc/TestValueInfo.php
  51. 107
      tests/cases/REST/Miniflux/TestV1.php
  52. 48
      tests/cases/REST/NextcloudNews/TestV1_2.php
  53. 8
      tests/cases/REST/TestREST.php
  54. 24
      tests/cases/REST/TinyTinyRSS/TestAPI.php
  55. 2
      tests/cases/REST/TinyTinyRSS/TestIcon.php
  56. 6
      tests/cases/Service/TestSerial.php
  57. 8
      tests/cases/Service/TestService.php
  58. 295
      tests/lib/AbstractTest.php
  59. 4
      tests/lib/DatabaseDrivers/MySQL.php
  60. 4
      tests/lib/DatabaseDrivers/MySQLPDO.php

13
.php-cs-fixer.dist.php

@ -1,4 +1,4 @@
<?php
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
@ -20,6 +20,8 @@ $paths = [
BASE."tests/server.php",
];
$rules = [
// PSR standard to apply
'@PSR12' => true,
// house rules where PSR series is silent
'align_multiline_comment' => ['comment_type' => "phpdocs_only"],
'array_syntax' => ['syntax' => "short"],
@ -56,15 +58,18 @@ $rules = [
'trailing_comma_in_multiline' => ['elements' => ["arrays"]],
'unary_operator_spaces' => true,
'yoda_style' => false,
// PSR standard to apply
'@PSR12' => true,
// house exceptions to PSR rules
'curly_braces_position' => [
'functions_opening_brace' => "same_line",
'classes_opening_brace' => "same_line",
],
'function_declaration' => ['closure_function_spacing' => "none"],
'new_with_braces' => false, // no option to specify absence of braces
'new_with_braces' => [
'anonymous_class' => false,
'named_class' => false,
],
'single_blank_line_before_namespace' => false,
'blank_line_after_opening_tag' => false,
];
$finder = \PhpCsFixer\Finder::create();

2
UPGRADING

@ -9,7 +9,7 @@ usually prudent:
- Check for any changes to sample systemd unit or other init files
- If installing from source, update dependencies with:
`composer install -o --no-dev`
Upgrading from 0.10.4 to 0.10.?
=============================

12
docs/en/020_Getting_Started/050_Configuration.md

@ -385,7 +385,7 @@ Arsse/0.6.0 (Linux 4.15.0; x86_64; https://thearsse.com/)
|------------------|-----------|
| interval or null | `"PT24H"` |
How long to keep a newsfeed and its articles in the database after all its subscriptions have been deleted. Specifying `null` will retain unsubscribed newsfeeds forever, whereas an interval evaluating to zero (e.g. `"PT0S"`) will delete them immediately.
How long to keep a newsfeed subscription and its articles in the database after it has been soft-deleted by its owner. Specifying `null` will retain unsubscribed newsfeeds forever, whereas an interval evaluating to zero (e.g. `"PT0S"`) will delete them immediately.
Note that articles of orphaned newsfeeds are still subject to the `purgeArticleUnread` threshold below.
@ -395,11 +395,9 @@ Note that articles of orphaned newsfeeds are still subject to the `purgeArticleU
|------------------|---------|
| interval or null | `"P7D"` |
How long to keep a an article in the database after all users subscribed to its newsfeed have read it. Specifying `null` will retain articles up to the `purgeArticlesUnread` threshold below, whereas an interval evaluating to zero (e.g. `"PT0S"`) will delete them immediately.
How long to keep a an article in the database after it has been read. Specifying `null` will retain articles up to the `purgeArticlesUnread` threshold below, whereas an interval evaluating to zero (e.g. `"PT0S"`) will delete them immediately.
If an article is starred by any user, it is retained indefinitely regardless of this setting.
This setting also governs when an article is hidden from a user after being read by that user, regardless of its actual presence in the database.
If an article is starred by its owner, it is retained indefinitely regardless of this setting.
### purgeArticlesUnread
@ -407,9 +405,9 @@ This setting also governs when an article is hidden from a user after being read
|------------------|----------|
| interval or null | `"P21D"` |
How long to keep a an article in the database regardless of whether any users have read it. Specifying `null` will retain articles forever, whereas an interval evaluating to zero (e.g. `"PT0S"`) will delete them immediately.
How long to keep a an article in the database regardless of whether it has been read. Specifying `null` will retain articles forever, whereas an interval evaluating to zero (e.g. `"PT0S"`) will delete them immediately.
If an article is starred by any user, it is retained indefinitely regardless of this setting.
If an article is starred by its owner, it is retained indefinitely regardless of this setting.
# Obsolete settings

1
lib/AbstractException.php

@ -39,6 +39,7 @@ abstract class AbstractException extends \Exception {
"Db/Exception.updateFileUnreadable" => 10216,
"Db/Exception.updateFileError" => 10217,
"Db/Exception.updateFileIncomplete" => 10218,
"Db/Exception.updateSchemaDowngrade" => 10219,
"Db/Exception.paramTypeInvalid" => 10221,
"Db/Exception.paramTypeUnknown" => 10222,
"Db/Exception.paramTypeMissing" => 10223,

2
lib/CLI.php

@ -106,7 +106,7 @@ USAGE_TEXT;
}
return 0;
case "feed refresh":
return (int) !Arsse::$db->feedUpdate((int) $args['<n>'], true);
return (int) !Arsse::$db->subscriptionUpdate(null, (int) $args['<n>'], true);
case "feed refresh-all":
Arsse::$obj->get(Service::class)->watch(false);
return 0;

4
lib/Conf.php

@ -95,10 +95,10 @@ class Conf {
/** @var string|null User-Agent string to use when fetching feeds from foreign servers */
public $fetchUserAgentString = null;
/** @var \DateInterval|null When to delete a feed from the database after all its subscriptions have been deleted, as an ISO 8601 duration (default: 24 hours; null for never)
/** @var \DateInterval|null When to delete a subscription from the database after it has been soft-deleted, as an ISO 8601 duration (default: 24 hours; null for never)
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
public $purgeFeeds = "PT24H";
/** @var \DateInterval|null When to delete an unstarred article in the database after it has been marked read by all users, as an ISO 8601 duration (default: 7 days; null for never)
/** @var \DateInterval|null When to delete an unstarred article in the database after it has been marked read, as an ISO 8601 duration (default: 7 days; null for never)
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
public $purgeArticlesRead = "P7D";
/** @var \DateInterval|null When to delete an unstarred article in the database regardless of its read state, as an ISO 8601 duration (default: 21 days; null for never)

642
lib/Database.php

File diff suppressed because it is too large

4
lib/Db/AbstractDriver.php

@ -205,4 +205,8 @@ abstract class AbstractDriver implements Driver {
public function prepare(string $query, ...$paramType): Statement {
return $this->prepareArray($query, $paramType);
}
public function stringOutput(): bool {
return false;
}
}

4
lib/Db/Driver.php

@ -73,6 +73,7 @@ interface Driver {
* The tokens the implementation must understand are:
*
* - "greatest": the GREATEST function implemented by PostgreSQL and MySQL
* - "least": the LEAST function implemented by PostgreSQL and MySQL
* - "nocase": the name of a general-purpose case-insensitive collation sequence
* - "like": the case-insensitive LIKE operator
* - "integer": the integer type to use for explicit casts
@ -92,4 +93,7 @@ interface Driver {
* This should be restricted to quick maintenance; in SQLite terms it might include ANALYZE, but not VACUUM
*/
public function maintenance(): bool;
/** Reports whether the implementation will coerce integer and float values to text (string) */
public function stringOutput(): bool;
}

6
lib/Db/MySQL/Driver.php

@ -13,7 +13,7 @@ use JKingWeb\Arsse\Db\Exception;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
use ExceptionBuilder;
protected const SQL_MODE = "ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,STRICT_ALL_TABLES";
protected const SQL_MODE = "ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,STRICT_ALL_TABLES,NO_UNSIGNED_SUBTRACTION";
protected const TRANSACTIONAL_LOCKS = false;
/** @var \mysqli */
@ -84,6 +84,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
return '"utf8mb4_unicode_ci"';
case "asc":
return "";
case "integer":
return "signed integer";
default:
return $token;
}
@ -166,7 +168,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
$drv->report_mode = \MYSQLI_REPORT_OFF;
$this->db = mysqli_init();
$this->db->options(\MYSQLI_SET_CHARSET_NAME, "utf8mb4");
$this->db->options(\MYSQLI_OPT_INT_AND_FLOAT_NATIVE, false);
$this->db->options(\MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
$this->db->options(\MYSQLI_OPT_CONNECT_TIMEOUT, ceil(Arsse::$conf->dbTimeoutConnect));
@$this->db->real_connect($host, $user, $password, $db, $port, $socket);
if ($this->db->connect_errno) {

2
lib/Db/MySQL/PDODriver.php

@ -30,7 +30,7 @@ class PDODriver extends Driver {
try {
$this->db = new \PDO($dsn, $user, $password, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_STRINGIFY_FETCHES => true,
\PDO::ATTR_STRINGIFY_FETCHES => false,
]);
} catch (\PDOException $e) {
$msg = $e->getMessage();

4
lib/Db/PostgreSQL/Driver.php

@ -233,4 +233,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
$this->exec("ANALYZE");
return true;
}
public function stringOutput(): bool {
return true;
}
}

4
lib/Db/PostgreSQL/PDODriver.php

@ -61,4 +61,8 @@ class PDODriver extends Driver {
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
return new PDOStatement($this->db, $query, $paramTypes);
}
public function stringOutput(): bool {
return false;
}
}

4
lib/Db/SQLite3/PDODriver.php

@ -82,4 +82,8 @@ class PDODriver extends AbstractPDODriver {
}
}
}
public function stringOutput(): bool {
return true;
}
}

18
lib/Feed.php

@ -30,7 +30,6 @@ class Feed {
public $items = [];
public $newItems = [];
public $changedItems = [];
public $filteredItems = [];
public static function discover(string $url, string $username = '', string $password = ''): string {
// fetch the candidate feed
@ -84,9 +83,6 @@ class Feed {
if (!sizeof($this->newItems) && !sizeof($this->changedItems)) {
$this->modified = false;
} else {
if ($feedID) {
$this->computeFilterRules($feedID);
}
// if requested, scrape full content for any new and changed items
if ($scrape) {
$this->scrape();
@ -466,18 +462,4 @@ class Feed {
}
}
}
protected function computeFilterRules(int $feedID): void {
$rules = Arsse::$db->feedRulesGet($feedID);
foreach ($rules as $user => $r) {
$stats = ['new' => [], 'changed' => []];
foreach ($this->newItems as $index => $item) {
$stats['new'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories);
}
foreach ($this->changedItems as $index => $item) {
$stats['changed'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories);
}
$this->filteredItems[$user] = $stats;
}
}
}

44
lib/ImportExport/AbstractImportExport.php

@ -35,9 +35,18 @@ abstract class AbstractImportExport {
$folderMap[$f['parent']][$f['name']] = true;
}
}
// get feed IDs for each URL, adding feeds where necessary
// add any new feeds, and try an initial fetch on them
$feedMap = [];
foreach ($feeds as $k => $f) {
$feeds[$k]['id'] = Arsse::$db->feedAdd(($f['url']));
try {
$feedMap[$k] = Arsse::$db->subscriptionReserve($user, $f['url']);
} catch (InputException $e) {
// duplication is not an error in this case
}
}
foreach ($feedMap as $f) {
// this may fail with an exception, halting the process before visible modifications are made to the database
Arsse::$db->subscriptionUpdate($user, $f, true);
}
// start a transaction for atomic rollback
$tr = Arsse::$db->begin();
@ -62,27 +71,26 @@ abstract class AbstractImportExport {
}
}
// process newsfeed subscriptions
$feedMap = [];
$tagMap = [];
foreach ($feeds as $f) {
foreach ($feeds as $k => $f) {
$folder = $folderMap[$f['folder']];
$title = strlen(trim($f['title'])) ? $f['title'] : null;
$found = false;
// find a match for the import feed is existing subscriptions
foreach ($feedsDb as $db) {
if ((int) $db['feed'] == $f['id']) {
$found = true;
$feedMap[$f['id']] = (int) $db['id'];
break;
$new = false;
// find a match for the import feed in existing subscriptions, if necessary; reveal the subscription if it's just been added
if (!isset($feedMap[$k])) {
foreach ($feedsDb as $db) {
if ($db['url'] === $f['url']) {
$feedMap[$k] = (int) $db['id'];
break;
}
}
} else {
$new = true;
Arsse::$db->subscriptionReveal($user, $feedMap[$k]);
}
if (!$found) {
// if no subscription exists, add one
$feedMap[$f['id']] = Arsse::$db->subscriptionAdd($user, $f['url']);
}
if (!$found || $replace) {
if ($new || $replace) {
// set the subscription's properties, if this is a new feed or we're doing a full replacement
Arsse::$db->subscriptionPropertiesSet($user, $feedMap[$f['id']], ['title' => $title, 'folder' => $folder]);
Arsse::$db->subscriptionPropertiesSet($user, $feedMap[$k], ['title' => $title, 'folder' => $folder]);
// compile the set of used tags, if this is a new feed or we're doing a full replacement
foreach ($f['tags'] as $t) {
if (!strlen(trim($t))) {
@ -93,7 +101,7 @@ abstract class AbstractImportExport {
// populate the tag map
$tagMap[$t] = [];
}
$tagMap[$t][] = $f['id'];
$tagMap[$t][] = $feedMap[$k];
}
}
}

4
lib/REST.php

@ -126,14 +126,14 @@ class REST {
$target = substr($url, strlen($api['strip']));
} else {
// if the match fails we are not able to handle the request
throw new REST\Exception501();
throw new REST\Exception501;
}
// return the API name, stripped URL, and API class name
return [$id, $target, $api['class']];
}
}
// or throw an exception otherwise
throw new REST\Exception501();
throw new REST\Exception501;
}
public function authenticateRequest(ServerRequestInterface $req): ServerRequestInterface {

17
lib/REST/Miniflux/V1.php

@ -351,6 +351,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
// Miniflux does not attempt to coerce values into different types
foreach (self::VALID_JSON as $k => $t) {
if (!isset($body[$k])) {
// if a valid key is missing set it to null so that any key may be accessed safely
$body[$k] = null;
} elseif (gettype($body[$k]) !== $t) {
return self::respError(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422);
@ -814,16 +815,14 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
protected function createFeed(array $data): ResponseInterface {
$properties = [
'folder' => $data['category_id'] - 1,
'scrape' => (bool) $data['crawler'],
'keep_rule' => $data['keeplist_rules'],
'block_rule' => $data['blocklist_rules'],
];
try {
Arsse::$db->feedAdd($data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']);
$tr = Arsse::$db->begin();
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']);
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $data['category_id'] - 1, 'scrape' => (bool) $data['crawler']]);
$tr->commit();
if (strlen($data['keeplist_rules'] ?? "") || strlen($data['blocklist_rules'] ?? "")) {
// we do rules separately so as not to tie up the database
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['keep_rule' => $data['keeplist_rules'], 'block_rule' => $data['blocklist_rules']]);
}
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['feed_url'], (string) $data['username'], (string) $data['password'], false, $properties);
} catch (FeedException $e) {
$msg = [
10502 => "Fetch404",

2
lib/REST/NextcloudNews/V1_2.php

@ -378,7 +378,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
return HTTP::respEmpty(403);
}
try {
Arsse::$db->feedUpdate($data['feedId']);
Arsse::$db->subscriptionUpdate($data['userId'], $data['feedId']);
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
case 10239: // feed does not exist

2
lib/REST/TinyTinyRSS/API.php

@ -947,7 +947,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
throw new Exception("INCORRECT_USAGE");
}
try {
Arsse::$db->feedUpdate((int) Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, $data['feed_id'])['feed']);
Arsse::$db->subscriptionUpdate(Arsse::$user->id, $data['feed_id']);
} catch (ExceptionInput $e) {
throw new Exception("FEED_NOT_FOUND");
}

11
lib/Service.php

@ -22,13 +22,13 @@ class Service {
public function __construct() {
$driver = Arsse::$conf->serviceDriver;
$this->drv = new $driver();
$this->drv = new $driver;
}
public function watch(bool $loop = true): \DateTimeInterface {
$this->loop = $loop;
$this->signalInit();
$t = new \DateTime();
$t = new \DateTime;
do {
$this->checkIn();
static::cleanupPre();
@ -67,6 +67,7 @@ class Service {
}
public function checkIn(): bool {
Arsse::$db->checkSchemaVersion();
return Arsse::$db->metaSet("service_last_checkin", time(), "datetime");
}
@ -81,7 +82,7 @@ class Service {
// get the checking interval
$int = Arsse::$conf->serviceFrequency;
// subtract twice the checking interval from the current time to yield the earliest acceptable check-in time
$limit = new \DateTime();
$limit = new \DateTime;
$limit->sub($int);
$limit->sub($int);
// return whether the check-in time is within the acceptable limit
@ -89,8 +90,8 @@ class Service {
}
public static function cleanupPre(): bool {
// mark unsubscribed feeds as orphaned and delete orphaned feeds that are beyond their retention period
Arsse::$db->feedCleanup();
// delete soft-deleted subscriptions that are beyond their retention period
Arsse::$db->subscriptionCleanup();
// do the same for icons
Arsse::$db->iconCleanup();
// delete expired log-in sessions

2
lib/Service/Serial/Driver.php

@ -32,7 +32,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
public function exec(): int {
while (sizeof($this->queue)) {
$id = array_shift($this->queue);
Arsse::$db->feedUpdate($id);
Arsse::$db->subscriptionUpdate(null, $id);
}
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
}

1
locale/en.php

@ -147,6 +147,7 @@ return [
0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}}
other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
}',
'Exception.JKingWeb/Arsse/Db/Exception.updateSchemaDowngrade' => 'Database schema version is newer than the application schema version',
'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}',
// indicates programming error
'Exception.JKingWeb/Arsse/Db/Exception.savepointStatusUnknown' => 'Savepoint status code {0} not implemented',

2
sql/MySQL/0.sql

@ -31,7 +31,7 @@ create table arsse_folders(
owner varchar(255) not null references arsse_users(id) on delete cascade on update cascade,
parent bigint references arsse_folders(id) on delete cascade,
name varchar(255) not null,
modified datetime(0) not null default CURRENT_TIMESTAMP, --
modified datetime(0) not null default CURRENT_TIMESTAMP,
unique(owner,name,parent)
) character set utf8mb4;

184
sql/MySQL/7.sql

@ -0,0 +1,184 @@
-- SPDX-License-Identifier: MIT
-- Copyright 2017 J. King, Dustin Wilson et al.
-- See LICENSE and AUTHORS files for details
-- Create a temporary table mapping old article IDs to new article IDs per-user.
-- Any articles which have only one subscription will be unchanged, which will
-- limit the amount of disruption
create table arsse_articles_map(
article bigint unsigned not null,
subscription bigint unsigned not null,
id serial
);
-- alter table arsse_articles_map auto_increment = (select max(id) + 1 from arsse_articles);
insert into arsse_articles_map
select 0, 0, max(id) from arsse_articles;
delete from arsse_articles_map;
insert into arsse_articles_map(article, subscription)
select
a.id as article,
s.id as subscription
from arsse_articles as a join arsse_subscriptions as s using(feed)
where feed in (
select feed from (select feed, count(*) as count from arsse_subscriptions group by feed) as c where c.count > 1
)
order by a.id, s.id;
insert into arsse_articles_map(article, subscription, id)
select
a.id as article,
s.id as subscription,
a.id as id
from arsse_articles as a join arsse_subscriptions as s using(feed)
where feed in (
select feed from (select feed, count(*) as count from arsse_subscriptions group by feed) as c where c.count = 1
);
-- Add any new columns required for the articles table
alter table arsse_articles add column subscription bigint unsigned;
alter table arsse_articles add column "read" smallint not null default 0;
alter table arsse_articles add column starred smallint not null default 0;
alter table arsse_articles add column hidden smallint not null default 0;
alter table arsse_articles add column touched smallint not null default 0;
alter table arsse_articles add column marked datetime(0);
alter table arsse_articles add column note longtext;
-- Populate the articles table with new information; this either inserts or updates in-place
insert into arsse_articles(id,feed,subscription,"read",starred,hidden,published,edited,modified,marked,url,title,author,guid,url_title_hash,url_content_hash,title_content_hash,note)
select
i.id,
a.feed,
i.subscription,
coalesce(m."read",0),
coalesce(m.starred,0),
coalesce(m.hidden,0),
a.published,
a.edited,
a.modified,
m.modified,
a.url,
a.title,
a.author,
a.guid,
a.url_title_hash,
a.url_content_hash,
a.title_content_hash,
coalesce(m.note,'')
from arsse_articles_map as i
left join arsse_articles as a on a.id = i.article
left join arsse_marks as m on a.id = m.article and m.subscription = i.subscription
on duplicate key update
subscription = values(subscription),
"read" = values("read"),
starred = values(starred),
hidden = values(hidden),
marked = values(marked),
note = values(note);
-- Next create the subsidiary table to hold article contents
create table arsse_article_contents(
-- contents of articles, which is typically large text
id bigint unsigned primary key,
content longtext,
foreign key(id) references arsse_articles(id) on delete cascade on update cascade
) character set utf8mb4 collate utf8mb4_unicode_ci;
insert into arsse_article_contents
select
m.id,
case when s.scrape = 0 then a.content else coalesce(a.content_scraped, a.content) end
from arsse_articles_map as m
left join arsse_articles as a on a.id = m.article
left join arsse_subscriptions as s on s.id = m.subscription;
-- Drop the two content columns from the article table as they are no longer needed
alter table arsse_articles drop column content;
alter table arsse_articles drop column content_scraped;
-- Create one edition for each renumbered article
insert into arsse_editions(article, modified)
select
m.id, e.modified
from arsse_editions as e
join arsse_articles_map as m using(article)
where m.id <> article
order by m.id, modified;
-- Create enclures for renumbered articles
insert into arsse_enclosures(article, url, type)
select
m.id, url, type
from arsse_articles_map as m
join arsse_enclosures as e on m.article = e.article
where m.id <> m.article;
-- Create categories for renumbered articles
insert into arsse_categories(article, name)
select
m.id, name
from arsse_articles_map as m
join arsse_categories as c on m.article = c.article
where m.id <> m.article;
-- Create label associations for renumbered articles
insert into arsse_label_members
select
label, m.id, subscription, assigned, l.modified
from arsse_articles_map as m
join arsse_label_members as l using(article, subscription)
where m.id <> m.article;
-- Drop the subscription column from the label members table as it is no longer needed (there is now a direct link between articles and subscriptions)
alter table arsse_label_members drop foreign key arsse_label_members_ibfk_3;
alter table arsse_label_members drop column subscription;
-- Clean up the articles table: delete obsolete rows, add necessary constraints on new columns which could not be satisfied before inserting information, and drop the obsolete feed column
delete from arsse_articles where id in (select article from arsse_articles_map where id <> article);
delete from arsse_articles where subscription is null;
alter table arsse_articles modify subscription bigint unsigned not null;
alter table arsse_articles add foreign key(subscription) references arsse_subscriptions(id) on delete cascade on update cascade;
alter table arsse_articles drop foreign key arsse_articles_ibfk_1;
alter table arsse_articles drop column feed;
-- Add feed-related columns to the subscriptions table
alter table arsse_subscriptions add column url longtext;
alter table arsse_subscriptions add column feed_title longtext;
alter table arsse_subscriptions add column etag varchar(255) not null default '';
alter table arsse_subscriptions add column last_mod datetime(0);
alter table arsse_subscriptions add column next_fetch datetime(0);
alter table arsse_subscriptions add column updated datetime(0);
alter table arsse_subscriptions add column source longtext;
alter table arsse_subscriptions add column err_count bigint unsigned not null default 0;
alter table arsse_subscriptions add column err_msg longtext;
alter table arsse_subscriptions add column size bigint unsigned not null default 0;
alter table arsse_subscriptions add column icon bigint unsigned;
alter table arsse_subscriptions add column deleted boolean not null default 0;
-- Populate the new columns
update arsse_subscriptions as s, arsse_feeds as f set
s.url = f.url,
s.feed_title = f.title,
s.last_mod = f.modified,
s.etag = f.etag,
s.next_fetch = f.next_fetch,
s.source = f.source,
s.updated = f.updated,
s.err_count = f.err_count,
s.err_msg = f.err_msg,
s.size = f.size,
s.icon = f.icon
where s.feed = f.id;
-- Clean up the subscriptions table: add necessary constraints on new columns which could not be satisfied before inserting information, and drop the now obsolete feed column
alter table arsse_subscriptions modify url longtext not null;
alter table arsse_subscriptions add foreign key(icon) references arsse_icons(id) on delete set null;
alter table arsse_subscriptions add unique(owner,url(255));
alter table arsse_subscriptions drop constraint arsse_subscriptions_ibfk_2;
alter table arsse_subscriptions drop constraint owner;
alter table arsse_subscriptions drop column feed;
-- Delete unneeded table
drop table arsse_articles_map;
drop table arsse_marks;
drop table arsse_feeds;
-- set version marker
update arsse_meta set value = '8' where "key" = 'schema_version';

176
sql/PostgreSQL/7.sql

@ -0,0 +1,176 @@
-- SPDX-License-Identifier: MIT
-- Copyright 2017 J. King, Dustin Wilson et al.
-- See LICENSE and AUTHORS files for details
-- Create a temporary table mapping old article IDs to new article IDs per-user.
-- Any articles which have only one subscription will be unchanged, which will
-- limit the amount of disruption
create temp table arsse_articles_map(
article bigint not null,
subscription bigint not null,
id bigserial primary key
);
select setval('arsse_articles_map_id_seq', (select max(id) from arsse_articles));
insert into arsse_articles_map(article, subscription)
select
a.id as article,
s.id as subscription
from arsse_articles as a join arsse_subscriptions as s using(feed)
where feed in (
select feed from (select feed, count(*) as count from arsse_subscriptions group by feed) as c where c.count > 1
)
order by a.id, s.id;
insert into arsse_articles_map(article, subscription, id)
select
a.id as article,
s.id as subscription,
a.id as id
from arsse_articles as a join arsse_subscriptions as s using(feed)
where feed in (
select feed from (select feed, count(*) as count from arsse_subscriptions group by feed) as c where c.count = 1
);
-- Add any new columns required for the articles table
alter table arsse_articles add column subscription bigint references arsse_subscriptions(id) on delete cascade on update cascade;
alter table arsse_articles add column read smallint not null default 0;
alter table arsse_articles add column starred smallint not null default 0;
alter table arsse_articles add column hidden smallint not null default 0;
alter table arsse_articles add column touched smallint not null default 0;
alter table arsse_articles add column marked timestamp(0) without time zone;
alter table arsse_articles add column note text collate "und-x-icu" not null default '';
-- Populate the articles table with new information; this either inserts or updates in-place
with new_data as (
select
i.id,
a.feed,
i.subscription,
coalesce(m.read,0) as read,
coalesce(m.starred,0) as starred,
coalesce(m.hidden,0) as hidden,
a.published,
a.edited,
a.modified,
m.modified as marked,
a.url,
a.title,
a.author,
a.guid,
a.url_title_hash,
a.url_content_hash,
a.title_content_hash,
coalesce(m.note,'') as note
from arsse_articles_map as i
left join arsse_articles as a on a.id = i.article
left join arsse_marks as m on a.id = m.article and m.subscription = i.subscription
)
insert into arsse_articles(id,feed,subscription,read,starred,hidden,published,edited,modified,marked,url,title,author,guid,url_title_hash,url_content_hash,title_content_hash,note)
select * from new_data
on conflict (id) do update set (subscription,read,starred,hidden,marked,note) = (
select subscription, read, starred, hidden, marked, note from new_data where id = excluded.id
);
-- set the sequence number appropriately
select setval('arsse_articles_id_seq', (select max(id) from arsse_articles));
-- Next create the subsidiary table to hold article contents
create table arsse_article_contents(
-- contents of articles, which is typically large text
id bigint primary key references arsse_articles(id) on delete cascade on update cascade,
content text collate "und-x-icu"
);
insert into arsse_article_contents
select
m.id,
case when s.scrape = 0 then a.content else coalesce(a.content_scraped, a.content) end
from arsse_articles_map as m
left join arsse_articles as a on a.id = m.article
left join arsse_subscriptions as s on s.id = m.subscription;
-- Drop the two content columns from the article table as they are no longer needed
alter table arsse_articles drop column content_scraped;
alter table arsse_articles drop column content;
-- Create one edition for each renumbered article
insert into arsse_editions(article, modified)
select
m.id, e.modified
from arsse_editions as e
join arsse_articles_map as m using(article)
where m.id <> article
order by m.id, modified;
-- Create enclures for renumbered articles
insert into arsse_enclosures(article, url, type)
select
m.id, url, type
from arsse_articles_map as m
join arsse_enclosures as e on m.article = e.article
where m.id <> m.article;
-- Create categories for renumbered articles
insert into arsse_categories(article, name)
select
m.id, name
from arsse_articles_map as m
join arsse_categories as c on m.article = c.article
where m.id <> m.article;
-- Create label associations for renumbered articles
insert into arsse_label_members
select
label, m.id, subscription, assigned, l.modified
from arsse_articles_map as m
join arsse_label_members as l using(article, subscription)
where m.id <> m.article;
-- Drop the subscription column from the label members table as it is no longer needed (there is now a direct link between articles and subscriptions)
alter table arsse_label_members drop column subscription;
-- Clean up the articles table: delete obsolete rows, add necessary constraints on new columns which could not be satisfied before inserting information, and drop the obsolete feed column
delete from arsse_articles where id in (select article from arsse_articles_map where id <> article);
delete from arsse_articles where subscription is null;
alter table arsse_articles alter column subscription set not null;
alter table arsse_articles drop column feed;
-- Add feed-related columns to the subscriptions table
alter table arsse_subscriptions add column url text;
alter table arsse_subscriptions add column feed_title text collate "und-x-icu";
alter table arsse_subscriptions add column etag text not null default '';
alter table arsse_subscriptions add column last_mod timestamp(0) without time zone;
alter table arsse_subscriptions add column next_fetch timestamp(0) without time zone;
alter table arsse_subscriptions add column updated timestamp(0) without time zone;
alter table arsse_subscriptions add column source text;
alter table arsse_subscriptions add column err_count bigint not null default 0;
alter table arsse_subscriptions add column err_msg text collate "und-x-icu";
alter table arsse_subscriptions add column size bigint not null default 0;
alter table arsse_subscriptions add column icon bigint references arsse_icons(id) on delete set null;
alter table arsse_subscriptions add column deleted smallint not null default 0;
-- Populate the new columns
update arsse_subscriptions as s set
url = f.url,
feed_title = f.title,
last_mod = f.modified,
etag = f.etag,
next_fetch = f.next_fetch,
source = f.source,
updated = f.updated,
err_count = f.err_count,
err_msg = f.err_msg,
size = f.size,
icon = f.icon
from arsse_feeds as f
where s.feed = f.id;
-- Clean up the subscriptions table: add necessary constraints on new columns which could not be satisfied before inserting information, and drop the now obsolete feed column
alter table arsse_subscriptions alter column url set not null;
alter table arsse_subscriptions add unique(owner,url);
alter table arsse_subscriptions drop column feed;
-- Delete unneeded table
drop table arsse_articles_map;
drop table arsse_marks;
drop table arsse_feeds;
-- set version marker
update arsse_meta set value = '8' where "key" = 'schema_version';

203
sql/SQLite3/7.sql

@ -0,0 +1,203 @@
-- SPDX-License-Identifier: MIT
-- Copyright 2017 J. King, Dustin Wilson et al.
-- See LICENSE and AUTHORS files for details
-- Create a temporary table mapping old article IDs to new article IDs per-user.
-- Any articles which have only one subscription will be unchanged, which will
-- limit the amount of disruption
create temp table arsse_articles_map(
article int not null,
subscription int not null,
id integer primary key autoincrement
);
replace into temp.sqlite_sequence(name,seq) select 'arsse_articles_map', max(id) from arsse_articles;
insert into arsse_articles_map(article, subscription)
select
a.id as article,
s.id as subscription
from arsse_articles as a join arsse_subscriptions as s using(feed)
where feed in (
select feed from (select feed, count(*) as count from arsse_subscriptions group by feed) as c where count > 1
);
insert into arsse_articles_map(article, subscription, id)
select
a.id as article,
s.id as subscription,
a.id as id
from arsse_articles as a join arsse_subscriptions as s using(feed)
where feed in (
select feed from (select feed, count(*) as count from arsse_subscriptions group by feed) as c where count = 1
);
-- Create a new articles table which combines the marks table but does not include content
create table arsse_articles_new(
-- metadata for entries in newsfeeds, including user state
id integer primary key, -- sequence number
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- associated subscription
read int not null default 0, -- whether the article has been read
starred int not null default 0, -- whether the article is starred
hidden int not null default 0, -- whether the article should be excluded from selection by default
published text, -- time of original publication
edited text, -- time of last edit by author
modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database pursuant to an authorial edit
marked text, -- time at which an article was last modified by the user
url text, -- URL of article
title text collate nocase, -- article title
author text collate nocase, -- author's name
guid text, -- a nominally globally unique identifier for the article, from the feed
url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid
url_content_hash text not null, -- hash of URL + content + enclosure URL + enclosure content type; used when checking for updates and for identification if there is no guid
title_content_hash text not null, -- hash of title + content + enclosure URL + enclosure content type; used when checking for updates and for identification if there is no guid
touched int not null default 0, -- field used internally while marking; should normally be left as 0
note text not null default '' -- Tiny Tiny RSS freeform user note
);
insert into arsse_articles_new
select
i.id,
i.subscription,
coalesce(m.read,0),
coalesce(m.starred,0),
coalesce(m.hidden,0),
a.published,
a.edited,
a.modified,
m.modified,
a.url,
a.title,
a.author,
a.guid,
a.url_title_hash,
a.url_content_hash,
a.title_content_hash,
0,
coalesce(m.note,'')
from arsse_articles_map as i
left join arsse_articles as a on a.id = i.article
left join arsse_marks as m on a.id = m.article and m.subscription = i.subscription;
-- Create a new table to hold article content
create table arsse_article_contents(
-- contents of articles, which is typically large text
id integer primary key references arsse_articles(id) on delete cascade on update cascade, -- reference to the article ID
content text -- the contents
);
insert into arsse_article_contents
select
m.id,
case when s.scrape = 0 then a.content else coalesce(a.content_scraped, a.content) end
from arsse_articles_map as m
left join arsse_articles as a on a.id = m.article
left join arsse_subscriptions as s on s.id = m.subscription;
-- Create one edition for each renumbered article, and delete any editions for obsolete articles
insert into arsse_editions(article, modified)
select
m.id, e.modified
from arsse_editions as e
join arsse_articles_map as m using(article)
where m.id <> article
order by m.id, modified;
delete from arsse_editions where article in (select article from arsse_articles_map where id <> article) or article not in (select id from arsse_articles_map);
-- Create enclures for renumbered articles and delete obsolete enclosures
insert into arsse_enclosures(article, url, type)
select
m.id, url, type
from arsse_articles_map as m
join arsse_enclosures as e on m.article = e.article
where m.id <> m.article;
delete from arsse_enclosures where article in (select article from arsse_articles_map where id <> article) or article not in (select id from arsse_articles_map);
-- Create categories for renumbered articles and delete obsolete categories
insert into arsse_categories(article, name)
select
m.id, name
from arsse_articles_map as m
join arsse_categories as c on m.article = c.article
where m.id <> m.article;
delete from arsse_categories where article in (select article from arsse_articles_map where id <> article) or article not in (select id from arsse_articles_map);
-- Create a new label-associations table which omits the subscription column and populate it with new data
create table arsse_label_members_new(
-- label assignments for articles
label integer not null references arsse_labels(id) on delete cascade, -- label ID associated to an article; label IDs belong to a user
article integer not null references arsse_articles(id) on delete cascade, -- article associated to a label
assigned int not null default 1, -- whether the association is current, to support soft deletion
modified text not null default CURRENT_TIMESTAMP, -- time at which the association was last made or unmade
primary key(label,article) -- only one association of a given label to a given article
) without rowid;
insert into arsse_label_members_new
select
label, m.id, assigned, l.modified
from arsse_articles_map as m
join arsse_label_members as l using(article, subscription);
-- Create a new subscriptions table which combines the feeds table
create table arsse_subscriptions_new(
-- users' subscriptions to newsfeeds, with settings
id integer primary key, -- sequence number
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription
url text not null, -- URL of feed
deleted int not null default 0, -- soft-delete flag
feed_title text collate nocase, -- feed title
title text collate nocase, -- user-supplied title, which overrides the feed title when set
folder integer references arsse_folders(id) on delete cascade, -- TT-RSS category (nestable); the first-level category (which acts as Nextcloud folder) is joined in when needed
modified text not null default CURRENT_TIMESTAMP, -- time at which subscription properties were last modified by the user
last_mod text, -- time at which the feed last actually changed at the foreign host
etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes
next_fetch text, -- time at which the feed should next be fetched
added text not null default CURRENT_TIMESTAMP, -- time at which feed was added
source text, -- URL of site to which the feed belongs
updated text, -- time at which the feed was last fetched
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update
err_msg text, -- last error message
size integer not null default 0, -- number of articles in the feed at last fetch
icon integer references arsse_icons(id) on delete set null, -- numeric identifier of any associated icon
order_type int not null default 0, -- Nextcloud sort order
pinned int not null default 0, -- whether feed is pinned (always sorts at top)
scrape int not null default 0, -- whether the user has requested scraping content from source articles
keep_rule text, -- Regular expression the subscription's articles must match to avoid being hidden
block_rule text, -- Regular expression the subscription's articles must not match to avoid being hidden
unique(owner,url) -- a URL with particular credentials should only appear once
);
insert into arsse_subscriptions_new
select
s.id,
s.owner,
f.url,
0,
f.title,
s.title,
s.folder,
s.modified,
f.modified,
f.etag,
f.next_fetch,
s.added,
f.source,
f.updated,
f.err_count,
f.err_msg,
f.size,
f.icon,
s.order_type,
s.pinned,
s.scrape,
s.keep_rule,
s.block_rule
from arsse_subscriptions as s left join arsse_feeds as f on s.feed = f.id;
-- Delete the old tables and rename the new ones
drop table arsse_label_members;
drop table arsse_subscriptions;
drop table arsse_feeds;
drop table arsse_articles;
drop table arsse_marks;
drop table arsse_articles_map;
alter table arsse_subscriptions_new rename to arsse_subscriptions;
alter table arsse_articles_new rename to arsse_articles;
alter table arsse_label_members_new rename to arsse_label_members;
-- set version marker
pragma user_version = 8;
update arsse_meta set value = '8' where "key" = 'schema_version';

6
tests/cases/CLI/TestCLI.php

@ -121,11 +121,11 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideFeedUpdates */
public function testRefreshAFeed(string $cmd, int $exitStatus, string $output): void {
$this->dbMock->feedUpdate->with(1, true)->returns(true);
$this->dbMock->feedUpdate->with(2, true)->throws(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/"], $this->mockGuzzleException(ClientException::class, "", 404)));
$this->dbMock->subscriptionUpdate->with(null, 1, true)->returns(true);
$this->dbMock->subscriptionUpdate->with(null, 2, true)->throws(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/"], $this->mockGuzzleException(ClientException::class, "", 404)));
$this->assertConsole($cmd, $exitStatus, $output);
$this->cli->loadConf->called();
$this->dbMock->feedUpdate->called();
$this->dbMock->subscriptionUpdate->called();
}
public function provideFeedUpdates(): iterable {

795
tests/cases/Database/SeriesArticle.php

File diff suppressed because it is too large

196
tests/cases/Database/SeriesCleanup.php

@ -11,6 +11,8 @@ use JKingWeb\Arsse\Arsse;
use DateTimeImmutable as Date;
trait SeriesCleanup {
protected static $drv;
protected function setUpSeriesCleanup(): void {
// set up the configuration
Arsse::$conf->import([
@ -28,24 +30,15 @@ trait SeriesCleanup {
$faroff = (new Date("now + 1 hour", $tz))->format("Y-m-d H:i:s");
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
],
],
'arsse_sessions' => [
'columns' => [
'id' => "str",
'created' => "datetime",
'expires' => "datetime",
'user' => "str",
],
'rows' => [
'columns' => ["id", "created", "expires", "user"],
'rows' => [
["a", $nowish, $faroff, "jane.doe@example.com"], // not expired and recently created, thus kept
["b", $nowish, $soon, "jane.doe@example.com"], // not expired and recently created, thus kept
["c", $daysago, $soon, "jane.doe@example.com"], // created more than a day ago, thus deleted
@ -54,13 +47,8 @@ trait SeriesCleanup {
],
],
'arsse_tokens' => [
'columns' => [
'id' => "str",
'class' => "str",
'user' => "str",
'expires' => "datetime",
],
'rows' => [
'columns' => ["id", "class", "user", "expires"],
'rows' => [
["80fa94c1a11f11e78667001e673b2560", "fever.login", "jane.doe@example.com", $faroff],
["27c6de8da13311e78667001e673b2560", "fever.login", "jane.doe@example.com", $weeksago], // expired
["ab3b3eb8a13311e78667001e673b2560", "class.class", "jane.doe@example.com", null],
@ -68,99 +56,73 @@ trait SeriesCleanup {
],
],
'arsse_icons' => [
'columns' => [
'id' => "int",
'url' => "str",
'orphaned' => "datetime",
],
'rows' => [
'columns' => ["id", "url", "orphaned"],
'rows' => [
[1,'http://localhost:8000/Icon/PNG',$daybefore],
[2,'http://localhost:8000/Icon/GIF',$daybefore],
[3,'http://localhost:8000/Icon/SVG1',null],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
'orphaned' => "datetime",
'size' => "int",
'icon' => "int",
],
'rows' => [
[1,"http://example.com/1","",$daybefore,2,null], //latest two articles should be kept
[2,"http://example.com/2","",$yesterday,0,2],
[3,"http://example.com/3","",null,0,1],
[4,"http://example.com/4","",$nowish,0,null],
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
],
'rows' => [
// one feed previously marked for deletion has a subscription again, and so should not be deleted
[1,'jane.doe@example.com',1],
// other subscriptions exist for article cleanup tests
[2,'john.doe@example.com',1],
'columns' => ["id", "owner", "url", "size", "icon", "deleted", "modified"],
'rows' => [
// first two subscriptions are used for article cleanup tests: the latest two articles should be kept
[1,'jane.doe@example.com',"http://example.com/1",2,null,0,$daybefore],
[2,'john.doe@example.com',"http://example.com/1",2, 1,0,$daybefore],
// the other subscriptions are used for subscription cleanup
[3,'jane.doe@example.com',"http://example.com/2",0, 2,1,$yesterday],
[4,'jane.doe@example.com',"http://example.com/4",0,null,1,$nowish],
],
],
'arsse_articles' => [
'columns' => [
'id' => "int",
'feed' => "int",
'url_title_hash' => "str",
'url_content_hash' => "str",
'title_content_hash' => "str",
'modified' => "datetime",
],
'rows' => [
[1,1,"","","",$weeksago], // is the latest article, thus is kept
[2,1,"","","",$weeksago], // is the second latest article, thus is kept
[3,1,"","","",$weeksago], // is starred by one user, thus is kept
[4,1,"","","",$weeksago], // does not meet the unread threshold due to a recent mark, thus is kept
[5,1,"","","",$daysago], // does not meet the unread threshold due to age, thus is kept
[6,1,"","","",$weeksago], // does not meet the read threshold due to a recent mark, thus is kept
[7,1,"","","",$weeksago], // meets the unread threshold without marks, thus is deleted
[8,1,"","","",$weeksago], // meets the unread threshold even with marks, thus is deleted
[9,1,"","","",$weeksago], // meets the read threshold, thus is deleted
'columns' => ["id", "subscription", "url_title_hash", "url_content_hash", "title_content_hash", "modified", "read", "starred", "hidden", "marked"],
'rows' => [
[ 1,1,"","","",$weeksago,0,0,0,null], // is the latest article, thus is kept
[ 2,1,"","","",$weeksago,0,0,0,null], // is the second latest article, thus is kept
[ 3,1,"","","",$weeksago,0,1,0,$weeksago], // is starred by the user, thus is kept
[ 4,1,"","","",$weeksago,1,0,0,$yesterday], // does not meet the unread threshold due to a recent mark, thus is kept
[ 5,1,"","","",$daysago, 0,0,0,null], // does not meet the unread threshold due to age, thus is kept
[ 6,1,"","","",$weeksago,1,0,0,$nowish], // does not meet the read threshold due to a recent mark, thus is kept
[ 7,1,"","","",$weeksago,0,0,0,null], // meets the unread threshold without marks, thus is deleted
[ 8,1,"","","",$weeksago,1,0,0,$weeksago], // meets the unread threshold even with marks, thus is deleted
[ 9,1,"","","",$weeksago,1,0,0,$daysago], // meets the read threshold, thus is deleted
[1001,2,"","","",$weeksago,0,0,0,null], // is the latest article, thus is kept
[1002,2,"","","",$weeksago,0,0,0,null], // is the second latest article, thus is kept
[1003,2,"","","",$weeksago,0,0,0,null], // meets the unread threshold without marks, thus is deleted
[1004,2,"","","",$weeksago,0,0,0,null], // meets the unread threshold without marks, thus is deleted
[1005,2,"","","",$daysago, 0,0,0,null], // does not meet the unread threshold due to age, thus is kept
[1006,2,"","","",$weeksago,1,0,0,$weeksago], // meets the unread threshold even with marks, thus is deleted
[1007,2,"","","",$weeksago,0,1,1,$weeksago], // hidden overrides starred, thus is deleted
[1008,2,"","","",$weeksago,0,0,0,null], // meets the unread threshold without marks, thus is deleted
[1009,2,"","","",$weeksago,0,0,1,$daysago], // meets the read threshold because hidden is equivalent to read, thus is deleted
],
],
'arsse_editions' => [
'columns' => [
'id' => "int",
'article' => "int",
],
'rows' => [
'columns' => ["id", "article"],
'rows' => [
[1,1],
[2,2],
[3,3],
[4,4],
[5,5],
[6,6],
[7,7],
[8,8],
[9,9],
[201,1],
[102,2],
],
],
'arsse_marks' => [
'columns' => [
'article' => "int",
'subscription' => "int",
'read' => "bool",
'starred' => "bool",
'hidden' => "bool",
'modified' => "datetime",
],
'rows' => [
[3,1,0,1,0,$weeksago],
[4,1,1,0,0,$daysago],
[6,1,1,0,0,$nowish],
[6,2,1,0,0,$weeksago],
[7,2,0,1,1,$weeksago], // hidden takes precedence over starred
[8,1,1,0,0,$weeksago],
[9,1,1,0,0,$daysago],
[9,2,0,0,1,$daysago], // hidden is the same as read for the purposes of cleanup
[1001,1001],
[1002,1002],
[1003,1003],
[1004,1004],
[1005,1005],
[1006,1006],
[1007,1007],
[1008,1008],
[1009,1009],
[1201,1001],
[1102,1002],
],
],
];
@ -170,29 +132,23 @@ trait SeriesCleanup {
unset($this->data);
}
public function testCleanUpOrphanedFeeds(): void {
Arsse::$db->feedCleanup();
$now = gmdate("Y-m-d H:i:s");
public function testCleanUpDeletedSubscriptions(): void {
Arsse::$db->subscriptionCleanup();
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ["id","orphaned"],
'arsse_subscriptions' => ["id"],
]);
$state['arsse_feeds']['rows'][0][1] = null;
unset($state['arsse_feeds']['rows'][1]);
$state['arsse_feeds']['rows'][2][1] = $now;
unset($state['arsse_subscriptions']['rows'][2]);
$this->compareExpectations(static::$drv, $state);
}
public function testCleanUpOrphanedFeedsWithUnlimitedRetention(): void {
public function testCleanUpDeletedSubscriptionsWithUnlimitedRetention(): void {
Arsse::$conf->import([
'purgeFeeds' => null,
]);
Arsse::$db->feedCleanup();
$now = gmdate("Y-m-d H:i:s");
Arsse::$db->subscriptionCleanup();
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ["id","orphaned"],
'arsse_subscriptions' => ["id"],
]);
$state['arsse_feeds']['rows'][0][1] = null;
$state['arsse_feeds']['rows'][2][1] = $now;
$this->compareExpectations(static::$drv, $state);
}
@ -227,8 +183,12 @@ trait SeriesCleanup {
$state = $this->primeExpectations($this->data, [
'arsse_articles' => ["id"],
]);
foreach ([7,8,9] as $id) {
unset($state['arsse_articles']['rows'][$id - 1]);
$deleted = [7, 8, 9, 1003, 1004, 1006, 1007, 1008, 1009];
$stop = sizeof($state['arsse_articles']['rows']);
for ($a = 0; $a < $stop; $a++) {
if (in_array($state['arsse_articles']['rows'][$a][0], $deleted)) {
unset($state['arsse_articles']['rows'][$a]);
}
}
$this->compareExpectations(static::$drv, $state);
}
@ -241,8 +201,12 @@ trait SeriesCleanup {
$state = $this->primeExpectations($this->data, [
'arsse_articles' => ["id"],
]);
foreach ([7,8] as $id) {
unset($state['arsse_articles']['rows'][$id - 1]);
$deleted = [7, 8, 1003, 1004, 1006, 1007, 1008];
$stop = sizeof($state['arsse_articles']['rows']);
for ($a = 0; $a < $stop; $a++) {
if (in_array($state['arsse_articles']['rows'][$a][0], $deleted)) {
unset($state['arsse_articles']['rows'][$a]);
}
}
$this->compareExpectations(static::$drv, $state);
}
@ -255,8 +219,12 @@ trait SeriesCleanup {
$state = $this->primeExpectations($this->data, [
'arsse_articles' => ["id"],
]);
foreach ([9] as $id) {
unset($state['arsse_articles']['rows'][$id - 1]);
$deleted = [8, 9, 1006, 1007, 1009];
$stop = sizeof($state['arsse_articles']['rows']);
for ($a = 0; $a < $stop; $a++) {
if (in_array($state['arsse_articles']['rows'][$a][0], $deleted)) {
unset($state['arsse_articles']['rows'][$a]);
}
}
$this->compareExpectations(static::$drv, $state);
}

303
tests/cases/Database/SeriesFeed.php

@ -11,6 +11,7 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Test\Result;
trait SeriesFeed {
protected static $drv;
protected $matches;
protected function setUpSeriesFeed(): void {
@ -20,105 +21,75 @@ trait SeriesFeed {
$now = gmdate("Y-m-d H:i:s", strtotime("now"));
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "", 1],
["john.doe@example.com", "", 2],
["jane.doe@example.org", "", 3],
],
],
'arsse_icons' => [
'columns' => [
'id' => "int",
'url' => "str",
'type' => "str",
'data' => "blob",
],
'rows' => [
'columns' => ["id", "url", "type", "data"],
'rows' => [
[1,'http://localhost:8000/Icon/PNG','image/png',base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")],
[2,'http://localhost:8000/Icon/GIF','image/gif',base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")],
// this actually contains the data of SVG2, which will lead to a row update when retieved
[3,'http://localhost:8000/Icon/SVG1','image/svg+xml','<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><rect width="900" height="600" fill="#ED2939"/><rect width="600" height="600" fill="#fff"/><rect width="300" height="600" fill="#002395"/></svg>'],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
'err_count' => "int",
'err_msg' => "str",
'modified' => "datetime",
'next_fetch' => "datetime",
'size' => "int",
'icon' => "int",
],
'rows' => [
[1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,null],
[2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,null],
[3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,null],
[4,"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"",$past,$past,0,null],
[5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0,null],
// these feeds all test icon caching
[6,"http://localhost:8000/Feed/WithIcon/PNG",null,0,"",$past,$future,0,1], // no change when updated
[7,"http://localhost:8000/Feed/WithIcon/GIF",null,0,"",$past,$future,0,1], // icon ID 2 will be assigned to feed when updated
[8,"http://localhost:8000/Feed/WithIcon/SVG1",null,0,"",$past,$future,0,3], // icon ID 3 will be modified when updated
[9,"http://localhost:8000/Feed/WithIcon/SVG2",null,0,"",$past,$future,0,null], // icon ID 4 will be created and assigned to feed when updated
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
'keep_rule' => "str",
'block_rule' => "str",
],
'rows' => [
[1,'john.doe@example.com',1,null,'^Sport$'],
[2,'john.doe@example.com',2,"",null],
[3,'john.doe@example.com',3,'\w+',null],
[4,'john.doe@example.com',4,'\w+',"["], // invalid rule leads to both rules being ignored
[5,'john.doe@example.com',5,null,'and/or'],
[6,'jane.doe@example.com',1,'^(?i)[a-z]+','3|6'],
'columns' => ["id", "owner", "keep_rule", "block_rule", "url", "feed_title", "err_count", "err_msg", "last_mod", "next_fetch", "size", "icon"],
'rows' => [
[1, 'john.doe@example.com',null, '^Sport$',"http://localhost:8000/Feed/Matching/3", "Ook", 0,"", $past,$past, 0,null],
[2, 'john.doe@example.com',"", null, "http://localhost:8000/Feed/Matching/1", "Eek", 5,"There was an error last time",$past,$future,0,null],
[3, 'john.doe@example.com','\w+', null, "http://localhost:8000/Feed/Fetching/Error?code=404", "Ack", 0,"", $past,$now, 0,null],
[4, 'john.doe@example.com','\w+', "[", "http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"", $past,$past, 0,null], // invalid rule leads to both rules being ignored
[5, 'john.doe@example.com',null, 'and/or', "http://localhost:8000/Feed/Parsing/Valid", "Ooook",0,"", $past,$future,0,null],
[6, 'jane.doe@example.com','^(?i)[a-z]+','3|6', "http://localhost:8000/Feed/Matching/3", "Ook", 0,"", $past,$past, 0,null],
// these feeds all test icon caching
[16,'jane.doe@example.org',null, null, "http://localhost:8000/Feed/WithIcon/PNG", null, 0,"", $past,$future,0,1], // no change when updated
[17,'jane.doe@example.org',null, null, "http://localhost:8000/Feed/WithIcon/GIF", null, 0,"", $past,$future,0,1], // icon ID 2 will be assigned to feed when updated
[18,'jane.doe@example.org',null, null, "http://localhost:8000/Feed/WithIcon/SVG1", null, 0,"", $past,$future,0,3], // icon ID 3 will be modified when updated
[19,'jane.doe@example.org',null, null, "http://localhost:8000/Feed/WithIcon/SVG2", null, 0,"", $past,$future,0,null], // icon ID 4 will be created and assigned to feed when updated
],
],
'arsse_articles' => [
'columns' => [
'id' => "int",
'feed' => "int",
'url' => "str",
'title' => "str",
'author' => "str",
'published' => "datetime",
'edited' => "datetime",
'content' => "str",
'guid' => "str",
'url_title_hash' => "str",
'url_content_hash' => "str",
'title_content_hash' => "str",
'modified' => "datetime",
'columns' => ["id", "subscription", "url", "title", "author", "published", "edited", "guid", "url_title_hash", "url_content_hash", "title_content_hash", "modified", "read", "starred", "hidden", "marked"],
'rows' => [
[1, 1,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past,1,0,0,null],
[2, 1,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:00','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',$past,0,0,0,null],
[3, 1,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:00','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',$past,1,0,0,null],
[4, 1,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:00','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$past,0,1,0,null],
[5, 1,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:00','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',$past,0,0,0,null],
[6, 2,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past,0,0,0,null],
[7, 5,'', '', '','2000-01-01 00:00:00','2000-01-01 00:00:00','205e986f4f8b3acfa281227beadb14f5e8c32c8dae4737f888c94c0df49c56f8','', '', '', $past,0,0,0,null],
[11,6,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past,1,0,0,$past],
[12,6,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:00','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',$past,1,0,0,$past],
[13,6,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:00','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',$past,1,1,0,$past],
[14,6,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:00','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$past,1,0,1,$past],
[15,6,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:00','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',$past,1,1,0,$past],
],
'rows' => [
[1,1,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past],
[2,1,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:00','<p>Article content 2</p>','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',$past],
[3,1,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:00','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',$past],
[4,1,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:00','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$past],
[5,1,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:00','<p>Article content 5</p>','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',$past],
[6,2,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past],
[7,5,'', '', '','2000-01-01 00:00:00','2000-01-01 00:00:00','', '205e986f4f8b3acfa281227beadb14f5e8c32c8dae4737f888c94c0df49c56f8','', '', '', $past],
],
'arsse_article_contents' => [
'columns' => ["id", "content"],
'rows' => [
[1, '<p>Article content 1</p>'],
[2, '<p>Article content 2</p>'],
[3, '<p>Article content 3</p>'],
[4, '<p>Article content 4</p>'],
[5, '<p>Article content 5</p>'],
[6, '<p>Article content 1</p>'],
[7, ''],
[11,'<p>Article content 1</p>'],
[12,'<p>Article content 2</p>'],
[13,'<p>Article content 3</p>'],
[14,'<p>Article content 4</p>'],
[15,'<p>Article content 5</p>'],
],
],
'arsse_editions' => [
'columns' => [
'id' => "int",
'article' => "int",
'modified' => "datetime",
],
'rows' => [
'columns' => ["id", "article", "modified"],
'rows' => [
[1,1,$past],
[2,2,$past],
[3,3,$past],
@ -126,44 +97,15 @@ trait SeriesFeed {
[5,5,$past],
],
],
'arsse_marks' => [
'columns' => [
'article' => "int",
'subscription' => "int",
'read' => "bool",
'starred' => "bool",
'hidden' => "bool",
'modified' => "datetime",
],
'rows' => [
// Jane's marks
[1,6,1,0,0,$past],
[2,6,1,0,0,$past],
[3,6,1,1,0,$past],
[4,6,1,0,1,$past],
[5,6,1,1,0,$past],
// John's marks
[1,1,1,0,0,$past],
[3,1,1,0,0,$past],
[4,1,0,1,0,$past],
],
],
'arsse_enclosures' => [
'columns' => [
'article' => "int",
'url' => "str",
'type' => "str",
],
'rows' => [
'columns' => ["article", "url", "type"],
'rows' => [
[7,'http://example.com/png','image/png'],
],
],
'arsse_categories' => [
'columns' => [
'article' => "int",
'name' => "str",
],
'rows' => [
'columns' => ["article", "name"],
'rows' => [
[7,'Syrinx'],
],
],
@ -207,79 +149,88 @@ trait SeriesFeed {
$this->assertResult([['id' => 1]], Arsse::$db->feedMatchIds(1, ['e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda'])); // this ID appears in both feed 1 and feed 2; only one result should be returned
}
/** @dataProvider provideFilterRules */
public function testGetRules(int $in, array $exp): void {
$this->assertSame($exp, Arsse::$db->feedRulesGet($in));
}
public function provideFilterRules(): iterable {
return [
[1, ['jane.doe@example.com' => ['keep' => "`^(?i)[a-z]+`u", 'block' => "`3|6`u"], 'john.doe@example.com' => ['keep' => "", 'block' => "`^Sport$`u"]]],
[2, []],
[3, ['john.doe@example.com' => ['keep' => '`\w+`u', 'block' => ""]]],
[4, []],
[5, ['john.doe@example.com' => ['keep' => "", 'block' => "`and/or`u"]]],
];
}
public function testUpdateAFeed(): void {
// update a valid feed with both new and changed items
Arsse::$db->feedUpdate(1);
Arsse::$db->subscriptionUpdate(null, 1);
Arsse::$db->subscriptionUpdate(null, 6);
$now = gmdate("Y-m-d H:i:s");
$past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute"));
$state = $this->primeExpectations($this->data, [
'arsse_articles' => ["id", "feed","url","title","author","published","edited","content","guid","url_title_hash","url_content_hash","title_content_hash","modified"],
'arsse_editions' => ["id","article","modified"],
'arsse_marks' => ["subscription","article","read","starred","hidden","modified"],
'arsse_feeds' => ["id","size"],
'arsse_articles' => ["id", "subscription","url","title","author","published","edited","guid","url_title_hash","url_content_hash","title_content_hash","modified"],
'arsse_article_contents' => ["id", "content"],
'arsse_editions' => ["id","article","modified"],
'arsse_subscriptions' => ["id","size"],
]);
$state['arsse_articles']['rows'][2] = [3,1,'http://example.com/3','Article title 3 (updated)','','2000-01-03 00:00:00','2000-01-03 00:00:00','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','6cc99be662ef3486fef35a890123f18d74c29a32d714802d743c5b4ef713315a','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','d5faccc13bf8267850a1e8e61f95950a0f34167df2c8c58011c0aaa6367026ac',$now];
$state['arsse_articles']['rows'][3] = [4,1,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:01','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$now];
$state['arsse_articles']['rows'][] = [8,1,'http://example.com/6','Article title 6','','2000-01-06 00:00:00','2000-01-06 00:00:00','<p>Article content 6</p>','b3461ab8e8759eeb1d65a818c65051ec00c1dfbbb32a3c8f6999434e3e3b76ab','91d051a8e6749d014506848acd45e959af50bf876427c4f0e3a1ec0f04777b51','211d78b1a040d40d17e747a363cc283f58767b2e502630d8de9b8f1d5e941d18','5ed68ccb64243b8c1931241d2c9276274c3b1d87f223634aa7a1ab0141292ca7',$now];
$state['arsse_articles']['rows'][2] = [3,1,'http://example.com/3','Article title 3 (updated)','','2000-01-03 00:00:00','2000-01-03 00:00:00','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','6cc99be662ef3486fef35a890123f18d74c29a32d714802d743c5b4ef713315a','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','d5faccc13bf8267850a1e8e61f95950a0f34167df2c8c58011c0aaa6367026ac',$now];
$state['arsse_articles']['rows'][3] = [4,1,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:01','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$now];
$state['arsse_articles']['rows'][] = [16,1,'http://example.com/6','Article title 6','','2000-01-06 00:00:00','2000-01-06 00:00:00','b3461ab8e8759eeb1d65a818c65051ec00c1dfbbb32a3c8f6999434e3e3b76ab','91d051a8e6749d014506848acd45e959af50bf876427c4f0e3a1ec0f04777b51','211d78b1a040d40d17e747a363cc283f58767b2e502630d8de9b8f1d5e941d18','5ed68ccb64243b8c1931241d2c9276274c3b1d87f223634aa7a1ab0141292ca7',$now];
$state['arsse_articles']['rows'][9] = [13,6,'http://example.com/3','Article title 3 (updated)','','2000-01-03 00:00:00','2000-01-03 00:00:00','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','6cc99be662ef3486fef35a890123f18d74c29a32d714802d743c5b4ef713315a','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','d5faccc13bf8267850a1e8e61f95950a0f34167df2c8c58011c0aaa6367026ac',$now];
$state['arsse_articles']['rows'][10] = [14,6,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:01','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$now];
$state['arsse_articles']['rows'][] = [17,6,'http://example.com/6','Article title 6','','2000-01-06 00:00:00','2000-01-06 00:00:00','b3461ab8e8759eeb1d65a818c65051ec00c1dfbbb32a3c8f6999434e3e3b76ab','91d051a8e6749d014506848acd45e959af50bf876427c4f0e3a1ec0f04777b51','211d78b1a040d40d17e747a363cc283f58767b2e502630d8de9b8f1d5e941d18','5ed68ccb64243b8c1931241d2c9276274c3b1d87f223634aa7a1ab0141292ca7',$now];
$state['arsse_editions']['rows'] = array_merge($state['arsse_editions']['rows'], [
[6,8,$now],
[7,3,$now],
[8,4,$now],
[6, 16,$now],
[7, 3, $now],
[8, 4, $now],
[9, 17,$now],
[10,13,$now],
[11,14,$now],
]);
$state['arsse_article_contents']['rows'][2] = [3,'<p>Article content 3</p>'];
$state['arsse_article_contents']['rows'][3] = [4,'<p>Article content 4</p>'];
$state['arsse_article_contents']['rows'][] = [16,'<p>Article content 6</p>'];
$state['arsse_article_contents']['rows'][9] = [13,'<p>Article content 3</p>'];
$state['arsse_article_contents']['rows'][10] = [14,'<p>Article content 4</p>'];
$state['arsse_article_contents']['rows'][] = [17,'<p>Article content 6</p>'];
$state['arsse_subscriptions']['rows'][0] = [1,6];
$state['arsse_subscriptions']['rows'][5] = [6,6];
$this->compareExpectations(static::$drv, $state);
$state = $this->primeExpectations($this->data, [
'arsse_articles' => ["id", "read", "starred", "hidden", "marked"],
]);
$state['arsse_marks']['rows'][2] = [6,3,0,1,1,$now];
$state['arsse_marks']['rows'][3] = [6,4,0,0,0,$now];
$state['arsse_marks']['rows'][6] = [1,3,0,0,0,$now];
$state['arsse_marks']['rows'][] = [6,8,0,0,1,null];
$state['arsse_feeds']['rows'][0] = [1,6];
$state['arsse_articles']['rows'][2] = [3,0,0,0,null];
$state['arsse_articles']['rows'][9] = [13,0,1,1,$past];
$state['arsse_articles']['rows'][10] = [14,0,0,0,$past];
$state['arsse_articles']['rows'][] = [16,0,0,0,null];
$state['arsse_articles']['rows'][] = [17,0,0,1,null];
$this->compareExpectations(static::$drv, $state);
}
public function testUpdateAFeedWithErrors(): void {
// update a valid feed which previously had an error
Arsse::$db->feedUpdate(2);
Arsse::$db->subscriptionUpdate(null, 2);
// update an erroneous feed which previously had no errors
Arsse::$db->feedUpdate(3);
Arsse::$db->subscriptionUpdate(null, 3);
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ["id","err_count","err_msg"],
'arsse_subscriptions' => ["id","err_count","err_msg"],
]);
$state['arsse_feeds']['rows'][1] = [2,0,""];
$state['arsse_feeds']['rows'][2] = [3,1,'Feed URL "http://localhost:8000/Feed/Fetching/Error?code=404" is invalid'];
$state['arsse_subscriptions']['rows'][1] = [2,0,""];
$state['arsse_subscriptions']['rows'][2] = [3,1,'Feed URL "http://localhost:8000/Feed/Fetching/Error?code=404" is invalid'];
$this->compareExpectations(static::$drv, $state);
// update the bad feed again, twice
Arsse::$db->feedUpdate(3);
Arsse::$db->feedUpdate(3);
$state['arsse_feeds']['rows'][2] = [3,3,'Feed URL "http://localhost:8000/Feed/Fetching/Error?code=404" is invalid'];
Arsse::$db->subscriptionUpdate(null, 3);
Arsse::$db->subscriptionUpdate(null, 3);
$state['arsse_subscriptions']['rows'][2] = [3,3,'Feed URL "http://localhost:8000/Feed/Fetching/Error?code=404" is invalid'];
$this->compareExpectations(static::$drv, $state);
}
public function testUpdateAMissingFeed(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->feedUpdate(2112);
Arsse::$db->subscriptionUpdate(null, 2112);
}
public function testUpdateAnInvalidFeed(): void {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->feedUpdate(-1);
Arsse::$db->subscriptionUpdate(null, -1);
}
public function testUpdateAFeedThrowingExceptions(): void {
$this->assertException("invalidUrl", "Feed");
Arsse::$db->feedUpdate(3, true);
Arsse::$db->subscriptionUpdate(null, 3, true);
}
public function testUpdateAFeedWithEnclosuresAndCategories(): void {
Arsse::$db->feedUpdate(5);
Arsse::$db->subscriptionUpdate(null, 5);
$state = $this->primeExpectations($this->data, [
'arsse_enclosures' => ["url","type"],
'arsse_categories' => ["name"],
@ -299,48 +250,48 @@ trait SeriesFeed {
}
public function testListStaleFeeds(): void {
$this->assertEquals([1,3,4], Arsse::$db->feedListStale());
Arsse::$db->feedUpdate(3);
Arsse::$db->feedUpdate(4);
$this->assertEquals([1], Arsse::$db->feedListStale());
$this->assertEquals([1,3,4, 6], Arsse::$db->feedListStale());
Arsse::$db->subscriptionUpdate(null, 3);
Arsse::$db->subscriptionUpdate(null, 4);
$this->assertEquals([1, 6], Arsse::$db->feedListStale());
}
public function testCheckIconDuringFeedUpdate(): void {
Arsse::$db->feedUpdate(6);
Arsse::$db->subscriptionUpdate(null, 16);
$state = $this->primeExpectations($this->data, [
'arsse_icons' => ["id","url","type","data"],
'arsse_feeds' => ["id", "icon"],
'arsse_icons' => ["id","url","type","data"],
'arsse_subscriptions' => ["id", "icon"],
]);
$this->compareExpectations(static::$drv, $state);
}
public function testAssignIconDuringFeedUpdate(): void {
Arsse::$db->feedUpdate(7);
Arsse::$db->subscriptionUpdate(null, 17);
$state = $this->primeExpectations($this->data, [
'arsse_icons' => ["id","url","type","data"],
'arsse_feeds' => ["id", "icon"],
'arsse_icons' => ["id","url","type","data"],
'arsse_subscriptions' => ["id", "icon"],
]);
$state['arsse_feeds']['rows'][6][1] = 2;
$state['arsse_subscriptions']['rows'][7][1] = 2;
$this->compareExpectations(static::$drv, $state);
}
public function testChangeIconDuringFeedUpdate(): void {
Arsse::$db->feedUpdate(8);
Arsse::$db->subscriptionUpdate(null, 18);
$state = $this->primeExpectations($this->data, [
'arsse_icons' => ["id","url","type","data"],
'arsse_feeds' => ["id", "icon"],
'arsse_icons' => ["id","url","type","data"],
'arsse_subscriptions' => ["id", "icon"],
]);
$state['arsse_icons']['rows'][2][3] = '<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><rect fill="#fff" height="600" width="900"/><circle fill="#bc002d" cx="450" cy="300" r="180"/></svg>';
$this->compareExpectations(static::$drv, $state);
}
public function testAddIconDuringFeedUpdate(): void {
Arsse::$db->feedUpdate(9);
Arsse::$db->subscriptionUpdate(null, 19);
$state = $this->primeExpectations($this->data, [
'arsse_icons' => ["id","url","type","data"],
'arsse_feeds' => ["id", "icon"],
'arsse_icons' => ["id","url","type","data"],
'arsse_subscriptions' => ["id", "icon"],
]);
$state['arsse_feeds']['rows'][8][1] = 4;
$state['arsse_subscriptions']['rows'][9][1] = 4;
$state['arsse_icons']['rows'][] = [4,'http://localhost:8000/Icon/SVG2','image/svg+xml','<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><rect width="900" height="600" fill="#ED2939"/><rect width="600" height="600" fill="#fff"/><rect width="300" height="600" fill="#002395"/></svg>'];
$this->compareExpectations(static::$drv, $state);
}

71
tests/cases/Database/SeriesFolder.php

@ -10,26 +10,19 @@ namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Arsse;
trait SeriesFolder {
protected static $drv;
protected function setUpSeriesFolder(): void {
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
],
],
'arsse_folders' => [
'columns' => [
'id' => "int",
'owner' => "str",
'parent' => "int",
'name' => "str",
],
'columns' => ["id", "owner", "parent", "name"],
/* Layout translates to:
Jane
Politics
@ -49,47 +42,21 @@ trait SeriesFolder {
[6, "john.doe@example.com", 2, "Politics"],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
],
'rows' => [
[1,"http://example.com/1", "Feed 1"],
[2,"http://example.com/2", "Feed 2"],
[3,"http://example.com/3", "Feed 3"],
[4,"http://example.com/4", "Feed 4"],
[5,"http://example.com/5", "Feed 5"],
[6,"http://example.com/6", "Feed 6"],
[7,"http://example.com/7", "Feed 7"],
[8,"http://example.com/8", "Feed 8"],
[9,"http://example.com/9", "Feed 9"],
[10,"http://example.com/10", "Feed 10"],
[11,"http://example.com/11", "Feed 11"],
[12,"http://example.com/12", "Feed 12"],
[13,"http://example.com/13", "Feed 13"],
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
'folder' => "int",
],
'rows' => [
[1, "john.doe@example.com",1, null],
[2, "john.doe@example.com",2, null],
[3, "john.doe@example.com",3, 1],
[4, "john.doe@example.com",4, 6],
[5, "john.doe@example.com",5, 5],
[6, "john.doe@example.com",10, 5],
[7, "jane.doe@example.com",1, null],
[8, "jane.doe@example.com",10,null],
[9, "jane.doe@example.com",2, 4],
[10,"jane.doe@example.com",3, 4],
[11,"jane.doe@example.com",4, 4],
'columns' => ["id", "owner", "url", "title", "folder", "deleted"],
'rows' => [
[1, "john.doe@example.com", "http://example.com/1", "Feed 1", null, 0],
[2, "john.doe@example.com", "http://example.com/2", "Feed 2", null, 0],
[3, "john.doe@example.com", "http://example.com/3", "Feed 3", 1, 0],
[4, "john.doe@example.com", "http://example.com/4", "Feed 4", 6, 0],
[5, "john.doe@example.com", "http://example.com/5", "Feed 5", 5, 0],
[6, "john.doe@example.com", "http://example.com/10", "Feed 10", 5, 0],
[101, "john.doe@example.com", "http://example.com/101", "Feed 101", 1, 1],
[7, "jane.doe@example.com", "http://example.com/1", "Feed 1", null, 0],
[8, "jane.doe@example.com", "http://example.com/10", "Feed 10", null, 0],
[9, "jane.doe@example.com", "http://example.com/2", "Feed 2", 4, 0],
[10, "jane.doe@example.com", "http://example.com/3", "Feed 3", 4, 0],
[11, "jane.doe@example.com", "http://example.com/4", "Feed 4", 4, 0],
],
],
];

63
tests/cases/Database/SeriesIcon.php

@ -10,70 +10,37 @@ namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Arsse;
trait SeriesIcon {
protected static $drv;
protected function setUpSeriesIcon(): void {
// set up the test data
$past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute"));
$future = gmdate("Y-m-d H:i:s", strtotime("now + 1 minute"));
$now = gmdate("Y-m-d H:i:s", strtotime("now"));
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
],
],
'arsse_icons' => [
'columns' => [
'id' => "int",
'url' => "str",
'type' => "str",
'data' => "blob",
],
'rows' => [
'columns' => ["id", "url", "type", "data"],
'rows' => [
[1,'http://localhost:8000/Icon/PNG','image/png',base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")],
[2,'http://localhost:8000/Icon/GIF','image/gif',base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")],
[3,'http://localhost:8000/Icon/SVG1','image/svg+xml','<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><rect fill="#fff" height="600" width="900"/><circle fill="#bc002d" cx="450" cy="300" r="180"/></svg>'],
[4,'http://localhost:8000/Icon/SVG2','image/svg+xml','<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><rect width="900" height="600" fill="#ED2939"/><rect width="600" height="600" fill="#fff"/><rect width="300" height="600" fill="#002395"/></svg>'],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
'err_count' => "int",
'err_msg' => "str",
'modified' => "datetime",
'next_fetch' => "datetime",
'size' => "int",
'icon' => "int",
],
'rows' => [
[1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,1],
[2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,2],
[3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,3],
[4,"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"",$past,$past,0,null],
[5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0,2],
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
],
'rows' => [
[1,'john.doe@example.com',1],
[2,'john.doe@example.com',2],
[3,'john.doe@example.com',3],
[4,'john.doe@example.com',4],
[5,'john.doe@example.com',5],
[6,'jane.doe@example.com',5],
'columns' => ["id", "owner", "url", "title", "icon", "deleted"],
'rows' => [
[1,'john.doe@example.com',"http://localhost:8000/Feed/Matching/3", "Ook", 1, 0],
[2,'john.doe@example.com',"http://localhost:8000/Feed/Matching/1", "Eek", 2, 0],
[3,'john.doe@example.com',"http://localhost:8000/Feed/Fetching/Error?code=404", "Ack", 3, 0],
[4,'john.doe@example.com',"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(), "Ooook", null, 0],
[5,'john.doe@example.com',"http://localhost:8000/Feed/Parsing/Valid", "Ooook", 2, 0],
[6,'john.doe@example.com',"http://localhost:8000/Feed/Discovery/Valid", "Aaack", 4, 1],
[7,'jane.doe@example.com',"http://localhost:8000/Feed/Parsing/Valid", "Ooook", 2, 0],
],
],
];

363
tests/cases/Database/SeriesLabel.php

@ -12,17 +12,14 @@ use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Context\Context;
trait SeriesLabel {
protected static $drv;
protected $checkLabels;
protected function setUpSeriesLabel(): void {
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
["john.doe@example.org", "",3],
@ -30,13 +27,8 @@ trait SeriesLabel {
],
],
'arsse_folders' => [
'columns' => [
'id' => "int",
'owner' => "str",
'parent' => "int",
'name' => "str",
],
'rows' => [
'columns' => ["id", "owner", "parent", "name"],
'rows' => [
[1, "john.doe@example.com", null, "Technology"],
[2, "john.doe@example.com", 1, "Software"],
[3, "john.doe@example.com", 1, "Rocketry"],
@ -48,180 +40,164 @@ trait SeriesLabel {
[9, "john.doe@example.net", null, "Politics"],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
],
'rows' => [
[1,"http://example.com/1"],
[2,"http://example.com/2"],
[3,"http://example.com/3"],
[4,"http://example.com/4"],
[5,"http://example.com/5"],
[6,"http://example.com/6"],
[7,"http://example.com/7"],
[8,"http://example.com/8"],
[9,"http://example.com/9"],
[10,"http://example.com/10"],
[11,"http://example.com/11"],
[12,"http://example.com/12"],
[13,"http://example.com/13"],
'arsse_subscriptions' => [
'columns' => ["id", "owner", "url", "folder", "deleted"],
'rows' => [
[1, "john.doe@example.com", "http://example.com/1", null, 0],
[2, "john.doe@example.com", "http://example.com/2", null, 0],
[3, "john.doe@example.com", "http://example.com/3", 1, 0],
[4, "john.doe@example.com", "http://example.com/4", 6, 0],
[5, "john.doe@example.com", "http://example.com/10" ,5, 0],
[6, "jane.doe@example.com", "http://example.com/1", null, 0],
[7, "jane.doe@example.com", "http://example.com/10", null, 0],
[8, "john.doe@example.org", "http://example.com/11", null, 0],
[9, "john.doe@example.org", "http://example.com/12", null, 0],
[10, "john.doe@example.org", "http://example.com/13", null, 0],
[11, "john.doe@example.net", "http://example.com/10", null, 0],
[12, "john.doe@example.net", "http://example.com/2", 9, 0],
[13, "john.doe@example.net", "http://example.com/3", 8, 0],
[14, "john.doe@example.net", "http://example.com/4", 7, 0],
[16, "john.doe@example.com", "http://example.com/16", null, 1],
],
],
'arsse_subscriptions' => [
'arsse_articles' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
'folder' => "int",
"id", "subscription", "url", "title", "author", "published", "edited", "guid",
"url_title_hash", "url_content_hash", "title_content_hash", "modified",
"read", "starred", "hidden", "marked", "note",
],
'rows' => [
[1,"john.doe@example.com",1,null],
[2,"john.doe@example.com",2,null],
[3,"john.doe@example.com",3,1],
[4,"john.doe@example.com",4,6],
[5,"john.doe@example.com",10,5],
[6,"jane.doe@example.com",1,null],
[7,"jane.doe@example.com",10,null],
[8,"john.doe@example.org",11,null],
[9,"john.doe@example.org",12,null],
[10,"john.doe@example.org",13,null],
[11,"john.doe@example.net",10,null],
[12,"john.doe@example.net",2,9],
[13,"john.doe@example.net",3,8],
[14,"john.doe@example.net",4,7],
[1, 1,null, "Title one", null, null, null, null, "", "", "", "2000-01-01 00:00:00",1,1,0,'2000-01-01 00:00:00',''],
[2, 1,null, "Title two", null, null, null, null, "", "", "", "2010-01-01 00:00:00",0,0,0,'2010-01-01 00:00:00','Some Note'],
[3, 2,null, "Title three", null, null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,null, ''],
[4, 2,null, null, "John Doe",null, null, null, "", "", "", "2010-01-01 00:00:00",0,0,0,null, ''],
[5, 3,null, null, "John Doe",null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,null, ''],
[6, 3,null, null, "Jane Doe",null, null, null, "", "", "", "2010-01-01 00:00:00",0,0,1,'2000-01-01 00:00:00',''],
[7, 4,null, null, "Jane Doe",null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,null, ''],
[8, 4,null, null, null, null, null, null, "", "", "", "2010-01-01 00:00:00",0,0,1,null, ''],
[19, 5,null, null, null, null, null, null, "", "", "", "2000-01-01 00:00:00",1,0,0,'2016-01-01 00:00:00',''],
[20, 5,null, null, null, null, null, null, "", "", "", "2010-01-01 00:00:00",0,1,0,'2005-01-01 00:00:00',''],
[501, 6,null, "Title one", null, null, null, null, "", "", "", "2000-01-01 00:00:00",0,1,1,'2000-01-01 00:00:00',''],
[502, 6,null, "Title two", null, null, null, null, "", "", "", "2010-01-01 00:00:00",1,0,1,'2010-01-01 00:00:00',''],
[519, 7,null, null, null, null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,null, ''],
[520, 7,null, null, null, null, null, null, "", "", "", "2010-01-01 00:00:00",1,0,0,'2010-01-01 00:00:00',''],
[101, 8,'http://example.com/1','Article title 1','', '2000-01-01 00:00:00','2000-01-01 00:00:01','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00',0,0,0,null, ''],
[102, 8,'http://example.com/2','Article title 2','', '2000-01-02 00:00:00','2000-01-02 00:00:02','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00',1,0,0,'2000-01-02 02:00:00','Note 2'],
[103, 9,'http://example.com/3','Article title 3','', '2000-01-03 00:00:00','2000-01-03 00:00:03','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00',0,1,0,'2000-01-03 03:00:00','Note 3'],
[104, 9,'http://example.com/4','Article title 4','', '2000-01-04 00:00:00','2000-01-04 00:00:04','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00',1,1,0,'2000-01-04 04:00:00','Note 4'],
[105,10,'http://example.com/5','Article title 5','', '2000-01-05 00:00:00','2000-01-05 00:00:05','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00',0,0,0,'2000-01-05 05:00:00',''],
[119,11,null, null, null, null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,'2017-01-01 00:00:00','ook'],
[120,11,null, null, null, null, null, null, "", "", "", "2010-01-01 00:00:00",1,0,0,'2017-01-01 00:00:00','eek'],
[203,12,null, "Title three", null, null, null, null, "", "", "", "2000-01-01 00:00:00",0,1,0,'2017-01-01 00:00:00','ack'],
[204,12,null, null, "John Doe",null, null, null, "", "", "", "2010-01-01 00:00:00",1,1,0,'2017-01-01 00:00:00','ach'],
[205,13,null, null, "John Doe",null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,null, ''],
[206,13,null, null, "Jane Doe",null, null, null, "", "", "", "2010-01-01 00:00:00",0,0,0,null, ''],
[207,14,null, null, "Jane Doe",null, null, null, "", "", "", "2000-01-01 00:00:00",0,0,0,null, ''],
[208,14,null, null, null, null, null, null, "", "", "", "2010-01-01 00:00:00",0,0,0,null, ''],
[999,16,null, null, null, null, null, null, "", "", "", "2000-01-01 00:00:00",1,1,0,null, ''],
],
],
'arsse_articles' => [
'columns' => [
'id' => "int",
'feed' => "int",
'url' => "str",
'title' => "str",
'author' => "str",
'published' => "datetime",
'edited' => "datetime",
'content' => "str",
'guid' => "str",
'url_title_hash' => "str",
'url_content_hash' => "str",
'title_content_hash' => "str",
'modified' => "datetime",
],
'arsse_article_contents' => [
'columns' => ["id", "content"],
'rows' => [
[1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'],
[102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','<p>Article content 2</p>','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'],
[103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'],
[104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'],
[105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','<p>Article content 5</p>','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'],
[1, 'First article'],
[2, 'Second article'],
[3, 'third article'],
[4, ''],
[5, ''],
[6, ''],
[7, ''],
[8, ''],
[19, ''],
[20, ''],
[501,'First article'],
[502,'Second article'],
[519,''],
[520,''],
[101, '<p>Article content 1</p>'],
[102, '<p>Article content 2</p>'],
[103, '<p>Article content 3</p>'],
[104, '<p>Article content 4</p>'],
[105, '<p>Article content 5</p>'],
[119, ''],
[120, ''],
[203, 'third article'],
[204, ''],
[205, ''],
[206, ''],
[207, ''],
[208, ''],
],
],
'arsse_enclosures' => [
'columns' => [
'article' => "int",
'url' => "str",
'type' => "str",
'arsse_editions' => [
'columns' => ["id", "article"],
'rows' => [
[ 1, 1],
[ 2, 2],
[ 3, 3],
[ 4, 4],
[ 5, 5],
[ 6, 6],
[ 7, 7],
[ 8, 8],
[ 19, 19],
[ 20, 20],
[1001, 20],
[ 101,101],
[ 102,102],
[ 202,102],
[ 103,103],
[ 203,103],
[ 104,104],
[ 204,104],
[ 105,105],
[ 205,105],
[ 305,105],
[ 501,501],
[ 119,119],
[ 120,120],
[1101,120],
[2203,203],
[2204,204],
[2205,205],
[2206,206],
[2207,207],
[2208,208],
[ 502,502],
[ 519,519],
[ 520,520],
[1501,520],
],
'rows' => [
],
'arsse_enclosures' => [
'columns' => ["article", "url", "type"],
'rows' => [
[102,"http://example.com/text","text/plain"],
[103,"http://example.com/video","video/webm"],
[104,"http://example.com/image","image/svg+xml"],
[105,"http://example.com/audio","audio/ogg"],
],
],
'arsse_editions' => [
'columns' => [
'id' => "int",
'article' => "int",
],
'rows' => [
[1,1],
[2,2],
[3,3],
[4,4],
[5,5],
[6,6],
[7,7],
[8,8],
[9,9],
[10,10],
[11,11],
[12,12],
[13,13],
[14,14],
[15,15],
[16,16],
[17,17],
[18,18],
[19,19],
[20,20],
[101,101],
[102,102],
[103,103],
[104,104],
[105,105],
[202,102],
[203,103],
[204,104],
[205,105],
[305,105],
[1001,20],
],
],
'arsse_marks' => [
'columns' => [
'subscription' => "int",
'article' => "int",
'read' => "bool",
'starred' => "bool",
'modified' => "datetime",
'hidden' => "bool",
],
'rows' => [
[1, 1,1,1,'2000-01-01 00:00:00',0],
[5, 19,1,0,'2000-01-01 00:00:00',0],
[5, 20,0,1,'2010-01-01 00:00:00',0],
[7, 20,1,0,'2010-01-01 00:00:00',0],
[8, 102,1,0,'2000-01-02 02:00:00',0],
[9, 103,0,1,'2000-01-03 03:00:00',0],
[9, 104,1,1,'2000-01-04 04:00:00',0],
[10,105,0,0,'2000-01-05 05:00:00',0],
[11, 19,0,0,'2017-01-01 00:00:00',0],
[11, 20,1,0,'2017-01-01 00:00:00',0],
[12, 3,0,1,'2017-01-01 00:00:00',0],
[12, 4,1,1,'2017-01-01 00:00:00',0],
[4, 8,0,0,'2000-01-02 02:00:00',1],
'arsse_categories' => [ // author-supplied categories
'columns' => ["article", "name"],
'rows' => [
[19, "Fascinating"],
[19, "Logical"],
[20, "Interesting"],
[20, "Logical"],
[119,"Fascinating"],
[119,"Logical"],
[120,"Interesting"],
[120,"Logical"],
[519,"Fascinating"],
[519,"Logical"],
[520,"Interesting"],
[520,"Logical"],
],
],
'arsse_labels' => [
'columns' => [
'id' => "int",
'owner' => "str",
'name' => "str",
],
'rows' => [
'arsse_labels' => [ // labels applied to articles
'columns' => ["id", "owner", "name"],
'rows' => [
[1,"john.doe@example.com","Interesting"],
[2,"john.doe@example.com","Fascinating"],
[3,"jane.doe@example.com","Boring"],
@ -229,25 +205,22 @@ trait SeriesLabel {
],
],
'arsse_label_members' => [
'columns' => [
'label' => "int",
'article' => "int",
'subscription' => "int",
'assigned' => "bool",
],
'rows' => [
[1, 1,1,1],
[2, 1,1,1],
[1,19,5,1],
[2,20,5,1],
[1, 5,3,0],
[2, 5,3,1],
[2, 8,4,1],
'columns' => ["label", "article", "assigned"],
'rows' => [
[1, 1,1],
[2, 1,1],
[1, 19,1],
[2, 20,1],
[1, 5,0],
[2, 5,1],
[2, 8,1],
[1,999,1],
[2,999,1],
],
],
];
$this->checkLabels = ['arsse_labels' => ["id","owner","name"]];
$this->checkMembers = ['arsse_label_members' => ["label","article","subscription","assigned"]];
$this->checkMembers = ['arsse_label_members' => ["label","article","assigned"]];
$this->user = "john.doe@example.com";
}
@ -447,30 +420,30 @@ trait SeriesLabel {
public function testApplyALabelToArticles(): void {
Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5]));
$state = $this->primeExpectations($this->data, $this->checkMembers);
$state['arsse_label_members']['rows'][4][3] = 1;
$state['arsse_label_members']['rows'][] = [1,2,1,1];
$state['arsse_label_members']['rows'][4][2] = 1;
$state['arsse_label_members']['rows'][] = [1,2,1];
$this->compareExpectations(static::$drv, $state);
}
public function testClearALabelFromArticles(): void {
Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([1,5]), Database::ASSOC_REMOVE);
$state = $this->primeExpectations($this->data, $this->checkMembers);
$state['arsse_label_members']['rows'][0][3] = 0;
$state['arsse_label_members']['rows'][0][2] = 0;
$this->compareExpectations(static::$drv, $state);
}
public function testApplyALabelToArticlesByName(): void {
Arsse::$db->labelArticlesSet("john.doe@example.com", "Interesting", (new Context)->articles([2,5]), Database::ASSOC_ADD, true);
$state = $this->primeExpectations($this->data, $this->checkMembers);
$state['arsse_label_members']['rows'][4][3] = 1;
$state['arsse_label_members']['rows'][] = [1,2,1,1];
$state['arsse_label_members']['rows'][4][2] = 1;
$state['arsse_label_members']['rows'][] = [1,2,1];
$this->compareExpectations(static::$drv, $state);
}
public function testClearALabelFromArticlesByName(): void {
Arsse::$db->labelArticlesSet("john.doe@example.com", "Interesting", (new Context)->articles([1,5]), Database::ASSOC_REMOVE, true);
$state = $this->primeExpectations($this->data, $this->checkMembers);
$state['arsse_label_members']['rows'][0][3] = 0;
$state['arsse_label_members']['rows'][0][2] = 0;
$this->compareExpectations(static::$drv, $state);
}
@ -489,18 +462,18 @@ trait SeriesLabel {
public function testReplaceArticlesOfALabel(): void {
Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5]), Database::ASSOC_REPLACE);
$state = $this->primeExpectations($this->data, $this->checkMembers);
$state['arsse_label_members']['rows'][0][3] = 0;
$state['arsse_label_members']['rows'][2][3] = 0;
$state['arsse_label_members']['rows'][4][3] = 1;
$state['arsse_label_members']['rows'][] = [1,2,1,1];
$state['arsse_label_members']['rows'][0][2] = 0;
$state['arsse_label_members']['rows'][2][2] = 0;
$state['arsse_label_members']['rows'][4][2] = 1;
$state['arsse_label_members']['rows'][] = [1,2,1];
$this->compareExpectations(static::$drv, $state);
}
public function testPurgeArticlesOfALabel(): void {
Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([10000]), Database::ASSOC_REPLACE);
$state = $this->primeExpectations($this->data, $this->checkMembers);
$state['arsse_label_members']['rows'][0][3] = 0;
$state['arsse_label_members']['rows'][2][3] = 0;
$state['arsse_label_members']['rows'][0][2] = 0;
$state['arsse_label_members']['rows'][2][2] = 0;
$this->compareExpectations(static::$drv, $state);
}
}

13
tests/cases/Database/SeriesMeta.php

@ -11,16 +11,15 @@ use JKingWeb\Arsse\Test\Database;
use JKingWeb\Arsse\Arsse;
trait SeriesMeta {
protected static $drv;
protected function setUpSeriesMeta(): void {
$dataBare = [
'arsse_meta' => [
'columns' => [
'key' => 'str',
'value' => 'str',
],
'rows' => [
//['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION],
['album',"A Farewell to Kings"],
'columns' => ["key", "value"],
'rows' => [
//['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION],
['album',"A Farewell to Kings"],
],
],
];

2
tests/cases/Database/SeriesMiscellany.php

@ -11,6 +11,8 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database;
trait SeriesMiscellany {
protected static $drv;
protected function setUpSeriesMiscellany(): void {
static::setConf([
'dbDriver' => static::$dbDriverClass,

19
tests/cases/Database/SeriesSession.php

@ -11,6 +11,8 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Misc\Date;
trait SeriesSession {
protected static $drv;
protected function setUpSeriesSession(): void {
// set up the configuration
static::setConf([
@ -24,24 +26,15 @@ trait SeriesSession {
$old = gmdate("Y-m-d H:i:s", strtotime("now - 2 days"));
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
],
],
'arsse_sessions' => [
'columns' => [
'id' => "str",
'user' => "str",
'created' => "datetime",
'expires' => "datetime",
],
'rows' => [
'columns' => ["id", "user", "created", "expires"],
'rows' => [
["80fa94c1a11f11e78667001e673b2560", "jane.doe@example.com", $past, $faroff],
["27c6de8da13311e78667001e673b2560", "jane.doe@example.com", $past, $past], // expired
["ab3b3eb8a13311e78667001e673b2560", "jane.doe@example.com", $old, $future], // too old

412
tests/cases/Database/SeriesSubscription.php

@ -11,17 +11,16 @@ use GuzzleHttp\Exception\ClientException;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Test\Database;
use JKingWeb\Arsse\Feed\Exception as FeedException;
use JKingWeb\Arsse\Misc\Date;
trait SeriesSubscription {
protected static $drv;
public function setUpSeriesSubscription(): void {
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "", 1],
["john.doe@example.com", "", 2],
["jill.doe@example.com", "", 3],
@ -29,13 +28,8 @@ trait SeriesSubscription {
],
],
'arsse_folders' => [
'columns' => [
'id' => "int",
'owner' => "str",
'parent' => "int",
'name' => "str",
],
'rows' => [
'columns' => ["id", "owner", "parent", "name"],
'rows' => [
[1, "john.doe@example.com", null, "Technology"],
[2, "john.doe@example.com", 1, "Software"],
[3, "john.doe@example.com", 1, "Rocketry"],
@ -45,63 +39,27 @@ trait SeriesSubscription {
],
],
'arsse_icons' => [
'columns' => [
'id' => "int",
'url' => "str",
'data' => "blob",
],
'rows' => [
'columns' => ["id", "url", "data"],
'rows' => [
[1,"http://example.com/favicon.ico", "ICON DATA"],
[2,"http://example.net/favicon.ico", null],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
'username' => "str",
'password' => "str",
'updated' => "datetime",
'next_fetch' => "datetime",
'icon' => "int",
],
'rows' => [
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null],
[2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1],
[3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),2],
[4,"http://example.com/feed4", "Foo", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null],
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
'title' => "str",
'folder' => "int",
'pinned' => "bool",
'order_type' => "int",
'keep_rule' => "str",
'block_rule' => "str",
'scrape' => "bool",
],
'rows' => [
[1,"john.doe@example.com",2,null,null,1,2,null,null,0],
[2,"jane.doe@example.com",2,null,null,0,0,null,null,0],
[3,"john.doe@example.com",3,"Ook",2,0,1,null,null,0],
[4,"jill.doe@example.com",2,null,null,0,0,null,null,0],
[5,"jack.doe@example.com",2,null,null,1,2,"","3|E",0],
[6,"john.doe@example.com",4,"Bar",3,0,0,null,null,0],
'columns' => ["id", "owner", "url", "feed_title", "updated", "next_fetch", "icon", "title", "folder", "pinned", "order_type", "keep_rule", "block_rule", "scrape", "deleted", "modified"],
'rows' => [
[1, "john.doe@example.com", "http://example.com/feed2", "eek", Date::transform("now - 1 hour", "sql"), Date::transform("now - 1 hour", "sql"), 1, null, null, 1, 2, null, null, 0, 0, Date::transform("now - 1 hour", "sql")],
[2, "jane.doe@example.com", "http://example.com/feed2", "eek", Date::transform("now - 1 hour", "sql"), Date::transform("now - 1 hour", "sql"), 1, null, null, 0, 0, null, null, 0, 0, Date::transform("now - 1 hour", "sql")],
[3, "john.doe@example.com", "http://example.com/feed3", "Ack", Date::transform("now + 1 hour", "sql"), Date::transform("now + 1 hour", "sql"), 2, "Ook", 2, 0, 1, null, null, 0, 0, Date::transform("now - 1 hour", "sql")],
[4, "jill.doe@example.com", "http://example.com/feed2", "eek", Date::transform("now - 1 hour", "sql"), Date::transform("now - 1 hour", "sql"), 1, null, null, 0, 0, null, null, 0, 0, Date::transform("now - 1 hour", "sql")],
[5, "jack.doe@example.com", "http://example.com/feed2", "eek", Date::transform("now - 1 hour", "sql"), Date::transform("now - 1 hour", "sql"), 1, null, null, 1, 2, "", "3|E", 0, 0, Date::transform("now - 1 hour", "sql")],
[6, "john.doe@example.com", "http://example.com/feed4", "Foo", Date::transform("now + 1 hour", "sql"), Date::transform("now + 1 hour", "sql"), null, "Bar", 3, 0, 0, null, null, 0, 0, Date::transform("now - 1 hour", "sql")],
[7, "john.doe@example.com", "http://example.com/feed1", "ook", Date::transform("now + 6 hour", "sql"), Date::transform("now - 1 hour", "sql"), null, null, null, 0, 0, null, null, 0, 1, Date::transform("now - 1 hour", "sql")],
],
],
'arsse_tags' => [
'columns' => [
'id' => "int",
'owner' => "str",
'name' => "str",
],
'rows' => [
'columns' => ["id", "owner", "name"],
'rows' => [
[1,"john.doe@example.com","Interesting"],
[2,"john.doe@example.com","Fascinating"],
[3,"jane.doe@example.com","Boring"],
@ -109,12 +67,8 @@ trait SeriesSubscription {
],
],
'arsse_tag_members' => [
'columns' => [
'tag' => "int",
'subscription' => "int",
'assigned' => "bool",
],
'rows' => [
'columns' => ["tag", "subscription", "assigned"],
'rows' => [
[1,1,1],
[1,3,0],
[2,1,1],
@ -123,77 +77,83 @@ trait SeriesSubscription {
],
],
'arsse_articles' => [
'columns' => [
'id' => "int",
'feed' => "int",
'url_title_hash' => "str",
'url_content_hash' => "str",
'title_content_hash' => "str",
'title' => "str",
],
'rows' => [
[1,2,"","","","Title 1"],
[2,2,"","","","Title 2"],
[3,2,"","","","Title 3"],
[4,2,"","","","Title 4"],
[5,2,"","","","Title 5"],
[6,3,"","","","Title 6"],
[7,3,"","","","Title 7"],
[8,3,"","","","Title 8"],
'columns' => ["id", "subscription", "url_title_hash", "url_content_hash", "title_content_hash", "title", "read", "starred", "hidden"],
'rows' => [
[1, 1, "", "", "", "Title 1", 1, 0, 0],
[2, 1, "", "", "", "Title 2", 0, 0, 0],
[3, 1, "", "", "", "Title 3", 0, 0, 0],
[4, 1, "", "", "", "Title 4", 0, 0, 0],
[5, 1, "", "", "", "Title 5", 0, 0, 0],
[6, 2, "", "", "", "Title 1", 1, 0, 0],
[7, 2, "", "", "", "Title 2", 1, 0, 0],
[8, 2, "", "", "", "Title 3", 1, 0, 0],
[9, 2, "", "", "", "Title 4", 1, 0, 0],
[10, 2, "", "", "", "Title 5", 1, 0, 0],
[11, 4, "", "", "", "Title 1", 0, 0, 0],
[12, 4, "", "", "", "Title 2", 0, 0, 0],
[13, 4, "", "", "", "Title 3", 0, 0, 0],
[14, 4, "", "", "", "Title 4", 0, 0, 0],
[15, 4, "", "", "", "Title 5", 0, 0, 0],
[16, 5, "", "", "", "Title 1", 1, 0, 0],
[17, 5, "", "", "", "Title 2", 0, 0, 0],
[18, 5, "", "", "", "Title 3", 1, 0, 1],
[19, 5, "", "", "", "Title 4", 0, 0, 0],
[20, 5, "", "", "", "Title 5", 0, 0, 1],
[21, 3, "", "", "", "Title 6", 0, 0, 0],
[22, 3, "", "", "", "Title 7", 1, 0, 0],
[23, 3, "", "", "", "Title 8", 0, 0, 0],
],
],
'arsse_editions' => [
'columns' => [
'id' => "int",
'article' => "int",
],
'rows' => [
[1,1],
[2,2],
[3,3],
[4,4],
[5,5],
[6,6],
[7,7],
[8,8],
'columns' => ["id", "article"],
'rows' => [
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
[11, 11],
[12, 12],
[13, 13],
[14, 14],
[15, 15],
[16, 16],
[17, 17],
[18, 18],
[19, 19],
[20, 20],
[21, 21],
[22, 22],
[23, 23],
],
],
'arsse_categories' => [
'columns' => [
'article' => "int",
'name' => "str",
],
'rows' => [
[1,"A"],
[2,"B"],
[4,"D"],
[5,"E"],
[6,"F"],
[7,"G"],
[8,"H"],
],
],
'arsse_marks' => [
'columns' => [
'article' => "int",
'subscription' => "int",
'read' => "bool",
'starred' => "bool",
'hidden' => "bool",
],
'rows' => [
[1,2,1,0,0],
[2,2,1,0,0],
[3,2,1,0,0],
[4,2,1,0,0],
[5,2,1,0,0],
[1,1,1,0,0],
[7,3,1,0,0],
[8,3,0,0,0],
[1,5,1,0,0],
[3,5,1,0,1],
[4,5,0,0,0],
[5,5,0,0,1],
'columns' => ["article", "name"],
'rows' => [
[1, "A"],
[2, "B"],
[4, "D"],
[5, "E"],
[6, "A"],
[7, "B"],
[9, "D"],
[10, "E"],
[11, "A"],
[12, "B"],
[14, "D"],
[15, "E"],
[16, "A"],
[17, "B"],
[19, "D"],
[20, "E"],
[21, "F"],
[22, "G"],
[23, "H"],
],
],
];
@ -204,105 +164,99 @@ trait SeriesSubscription {
unset($this->data, $this->user);
}
public function testAddASubscriptionToAnExistingFeed(): void {
public function testReserveASubscription(): void {
$url = "http://example.com/feed5";
$exp = $this->nextID("arsse_subscriptions");
$act = Arsse::$db->subscriptionReserve($this->user, $url, "", "", false);
$this->assertSame($exp, $act);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][] = [$exp, $this->user, $url, 1, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
public function testReserveADeletedSubscription(): void {
$url = "http://example.com/feed1";
$subID = $this->nextID("arsse_subscriptions");
$db = $this->partialMock(Database::class, static::$drv);
$db->feedUpdate->returns(true);
Arsse::$db = $db->get();
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url));
$db->feedUpdate->never()->called();
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'],
]);
$state['arsse_subscriptions']['rows'][] = [$subID,$this->user,1];
$exp = 7;
$act = Arsse::$db->subscriptionReserve($this->user, $url, "", "", false);
$this->assertSame($exp, $act);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][6] = [$exp, $this->user, $url, 1, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
public function testAddASubscriptionToANewFeed(): void {
$url = "http://example.org/feed1";
$feedID = $this->nextID("arsse_feeds");
$subID = $this->nextID("arsse_subscriptions");
$db = $this->partialMock(Database::class, static::$drv);
$db->feedUpdate->returns(true);
Arsse::$db = $db->get();
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", false));
$db->feedUpdate->calledWith($feedID, true, false);
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'],
]);
$state['arsse_feeds']['rows'][] = [$feedID,$url,"",""];
$state['arsse_subscriptions']['rows'][] = [$subID,$this->user,$feedID];
public function testReserveASubscriptionWithPassword(): void {
$url = "http://john:secret@example.com/feed5";
$exp = $this->nextID("arsse_subscriptions");
$act = Arsse::$db->subscriptionReserve($this->user, "http://example.com/feed5", "john", "secret", false);
$this->assertSame($exp, $act);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][] = [$exp, $this->user, $url, 1, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
public function testAddASubscriptionToANewFeedViaDiscovery(): void {
$url = "http://localhost:8000/Feed/Discovery/Valid";
$discovered = "http://localhost:8000/Feed/Discovery/Feed";
$feedID = $this->nextID("arsse_feeds");
$subID = $this->nextID("arsse_subscriptions");
public function testReserveADuplicateSubscription(): void {
$url = "http://example.com/feed2";
$this->assertException("constraintViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionReserve($this->user, $url, "", "", false);
}
public function testReserveASubscriptionWithDiscovery(): void {
$exp = $this->nextID("arsse_subscriptions");
$act = Arsse::$db->subscriptionReserve($this->user, "http://localhost:8000/Feed/Discovery/Valid");
$this->assertSame($exp, $act);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][] = [$exp, $this->user, "http://localhost:8000/Feed/Discovery/Feed", 1, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
public function testRevealASubscription(): void {
$url = "http://example.com/feed1";
$this->assertNull(Arsse::$db->subscriptionReveal($this->user, 1, 7));
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][6] = [7, $this->user, $url, 0, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
public function testAddASubscription(): void {
$url = "http://example.org/feed5";
$id = $this->nextID("arsse_subscriptions");
$db = $this->partialMock(Database::class, static::$drv);
$db->feedUpdate->returns(true);
$db->subscriptionUpdate->returns(true);
$db->subscriptionPropertiesSet->returns(true);
Arsse::$db = $db->get();
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", true));
$db->feedUpdate->calledWith($feedID, true, false);
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'],
]);
$state['arsse_feeds']['rows'][] = [$feedID,$discovered,"",""];
$state['arsse_subscriptions']['rows'][] = [$subID,$this->user,$feedID];
$this->compareExpectations(static::$drv, $state);
try {
$this->assertSame($id, Arsse::$db->subscriptionAdd($this->user, $url, "", "", false, ['order_type' => 2]));
} finally {
$db->subscriptionUpdate->calledWith($this->user, $id, true);
$db->subscriptionPropertiesSet->calledWith($this->user, $id, ['order_type' => 2]);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][] = [$id, $this->user, $url, 0, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
}
public function testAddASubscriptionToAnInvalidFeed(): void {
$url = "http://example.org/feed1";
$feedID = $this->nextID("arsse_feeds");
$url = "http://example.org/feed5";
$id = $this->nextID("arsse_subscriptions");
$db = $this->partialMock(Database::class, static::$drv);
$db->feedUpdate->throws(new FeedException("", ['url' => $url], $this->mockGuzzleException(ClientException::class, "", 404)));
$db->subscriptionUpdate->throws(new FeedException("", ['url' => $url], $this->mockGuzzleException(ClientException::class, "", 404)));
$db->subscriptionPropertiesSet->returns(true);
Arsse::$db = $db->get();
$this->assertException("invalidUrl", "Feed");
try {
Arsse::$db->subscriptionAdd($this->user, $url, "", "", false);
Arsse::$db->subscriptionAdd($this->user, $url, "", "", false, ['order_type' => 2]);
} finally {
$db->feedUpdate->calledWith($feedID, true, false);
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'],
]);
$db->subscriptionUpdate->calledWith($this->user, $id, true);
$db->subscriptionPropertiesSet->calledWith($this->user, $id, ['order_type' => 2]);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$this->compareExpectations(static::$drv, $state);
}
}
public function testAddADuplicateSubscription(): void {
$url = "http://example.com/feed2";
$this->assertException("constraintViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionAdd($this->user, $url);
}
public function testAddADuplicateSubscriptionWithEquivalentUrl(): void {
$url = "http://EXAMPLE.COM/feed2";
$this->assertException("constraintViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionAdd($this->user, $url);
}
public function testAddADuplicateSubscriptionViaRedirection(): void {
$url = "http://localhost:8000/Feed/Parsing/Valid";
Arsse::$db->subscriptionAdd($this->user, $url);
$subID = $this->nextID("arsse_subscriptions");
$url = "http://localhost:8000/Feed/Fetching/RedirectionDuplicate";
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url));
}
public function testRemoveASubscription(): void {
$this->assertTrue(Arsse::$db->subscriptionRemove($this->user, 1));
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'],
]);
array_shift($state['arsse_subscriptions']['rows']);
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ["id", "owner", "url", "deleted", "modified"]]);
$state['arsse_subscriptions']['rows'][0] = [1, $this->user, "http://example.com/feed2", 1, Date::transform("now", "sql")];
$this->compareExpectations(static::$drv, $state);
}
@ -311,6 +265,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionRemove($this->user, 2112);
}
public function testRemoveADeletedSubscription(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionRemove($this->user, 7);
}
public function testRemoveAnInvalidSubscription(): void {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionRemove($this->user, -1);
@ -432,6 +391,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionPropertiesGet($this->user, 2112);
}
public function testGetThePropertiesOfADeletedSubscription(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesGet($this->user, 7);
}
public function testGetThePropertiesOfAnInvalidSubscription(): void {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesGet($this->user, -1);
@ -448,17 +412,16 @@ trait SeriesSubscription {
'block_rule' => "eek",
]);
$state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password','title'],
'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type','keep_rule','block_rule','scrape'],
'arsse_subscriptions' => ['id','owner','feed_title', 'title','folder','pinned','order_type','keep_rule','block_rule','scrape'],
]);
$state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,"Ook Ook",3,0,0,"ook","eek",1];
$state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com","eek","Ook Ook",3,0,0,"ook","eek",1];
$this->compareExpectations(static::$drv, $state);
Arsse::$db->subscriptionPropertiesSet($this->user, 1, [
'title' => null,
'keep_rule' => null,
'block_rule' => null,
]);
$state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0,null,null,1];
$state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com","eek",null,3,0,0,null,null,1];
$this->compareExpectations(static::$drv, $state);
// making no changes is a valid result
Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['unhinged' => true]);
@ -518,6 +481,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionIcon(null, -2112);
}
public function testRetrieveTheFaviconOfADeletedSubscription(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionIcon(null, 7);
}
public function testRetrieveTheFaviconOfASubscriptionWithUser(): void {
$exp = "http://example.com/favicon.ico";
$user = "john.doe@example.com";
@ -545,6 +513,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionTagsGet($this->user, 101);
}
public function testListTheTagsOfADeletedSubscription(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionTagsGet($this->user, 7);
}
public function testGetRefreshTimeOfASubscription(): void {
$user = "john.doe@example.com";
$this->assertTime(strtotime("now + 1 hour"), Arsse::$db->subscriptionRefreshed($user));
@ -553,14 +526,19 @@ trait SeriesSubscription {
public function testGetRefreshTimeOfAMissingSubscription(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
$this->assertTime(strtotime("now - 1 hour"), Arsse::$db->subscriptionRefreshed("john.doe@example.com", 2));
Arsse::$db->subscriptionRefreshed("john.doe@example.com", 2);
}
public function testGetRefreshTimeOfADeletedSubscription(): void {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionRefreshed("john.doe@example.com", 7);
}
public function testSetTheFilterRulesOfASubscriptionCheckingMarks(): void {
Arsse::$db->subscriptionPropertiesSet("jack.doe@example.com", 5, ['keep_rule' => "1|B|3|D", 'block_rule' => "4"]);
$state = $this->primeExpectations($this->data, ['arsse_marks' => ['article', 'subscription', 'hidden']]);
$state['arsse_marks']['rows'][9][2] = 0;
$state['arsse_marks']['rows'][10][2] = 1;
$state = $this->primeExpectations($this->data, ['arsse_articles' => ['id', 'hidden']]);
$state['arsse_articles']['rows'][17][1] = 0;
$state['arsse_articles']['rows'][18][1] = 1;
$this->compareExpectations(static::$drv, $state);
}
}

116
tests/cases/Database/SeriesTag.php

@ -11,77 +11,45 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database;
trait SeriesTag {
protected static $drv;
protected $checkMembers;
protected $checkTags;
protected function setUpSeriesTag(): void {
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
["john.doe@example.org", "",3],
["john.doe@example.net", "",4],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
],
'rows' => [
[1,"http://example.com/1",""],
[2,"http://example.com/2",""],
[3,"http://example.com/3","Feed Title"],
[4,"http://example.com/4",""],
[5,"http://example.com/5","Feed Title"],
[6,"http://example.com/6",""],
[7,"http://example.com/7",""],
[8,"http://example.com/8",""],
[9,"http://example.com/9",""],
[10,"http://example.com/10",""],
[11,"http://example.com/11",""],
[12,"http://example.com/12",""],
[13,"http://example.com/13",""],
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "", 1],
["john.doe@example.com", "", 2],
["john.doe@example.org", "", 3],
["john.doe@example.net", "", 4],
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'feed' => "int",
'title' => "str",
],
'rows' => [
[1, "john.doe@example.com", 1,"Lord of Carrots"],
[2, "john.doe@example.com", 2,null],
[3, "john.doe@example.com", 3,"Subscription Title"],
[4, "john.doe@example.com", 4,null],
[5, "john.doe@example.com",10,null],
[6, "jane.doe@example.com", 1,null],
[7, "jane.doe@example.com",10,null],
[8, "john.doe@example.org",11,null],
[9, "john.doe@example.org",12,null],
[10,"john.doe@example.org",13,null],
[11,"john.doe@example.net",10,null],
[12,"john.doe@example.net", 2,null],
[13,"john.doe@example.net", 3,null],
[14,"john.doe@example.net", 4,null],
'columns' => ["id", "owner", "url", "feed_title", "title", "deleted"],
'rows' => [
[1, "john.doe@example.com", "http://example.com/1", "", "Lord of Carrots", 0],
[2, "john.doe@example.com", "http://example.com/2", "", null, 0],
[3, "john.doe@example.com", "http://example.com/3", "Feed Title", "Subscription Title", 0],
[4, "john.doe@example.com", "http://example.com/4", "", null, 0],
[5, "john.doe@example.com", "http://example.com/10", "", null, 0],
[6, "jane.doe@example.com", "http://example.com/1", "", null, 0],
[7, "jane.doe@example.com", "http://example.com/10", "", null, 0],
[8, "john.doe@example.org", "http://example.com/11", "", null, 0],
[9, "john.doe@example.org", "http://example.com/12", "", null, 0],
[10, "john.doe@example.org", "http://example.com/13", "", null, 0],
[11, "john.doe@example.net", "http://example.com/10", "", null, 0],
[12, "john.doe@example.net", "http://example.com/2", "", null, 0],
[13, "john.doe@example.net", "http://example.com/3", "Feed Title", null, 0],
[14, "john.doe@example.net", "http://example.com/4", "", null, 0],
[16, "john.doe@example.com", "http://example.com/16", "", null, 1],
],
],
'arsse_tags' => [
'columns' => [
'id' => "int",
'owner' => "str",
'name' => "str",
],
'rows' => [
'columns' => ["id", "owner", "name"],
'rows' => [
[1,"john.doe@example.com","Interesting"],
[2,"john.doe@example.com","Fascinating"],
[3,"jane.doe@example.com","Boring"],
@ -89,18 +57,16 @@ trait SeriesTag {
],
],
'arsse_tag_members' => [
'columns' => [
'tag' => "int",
'subscription' => "int",
'assigned' => "bool",
],
'rows' => [
[1,1,1],
[1,3,0],
[1,5,1],
[2,1,1],
[2,3,1],
[2,5,1],
'columns' => ["tag", "subscription", "assigned"],
'rows' => [
[1, 1,1],
[1, 3,0],
[1, 5,1],
[2, 1,1],
[2, 3,1],
[2, 5,1],
[1,16,1],
[2,16,1],
],
],
];
@ -144,13 +110,13 @@ trait SeriesTag {
public function testListTags(): void {
$exp = [
['id' => 2, 'name' => "Fascinating"],
['id' => 1, 'name' => "Interesting"],
['id' => 4, 'name' => "Lonely"],
['id' => 2, 'name' => "Fascinating", 'subscriptions' => 3],
['id' => 1, 'name' => "Interesting", 'subscriptions' => 2],
['id' => 4, 'name' => "Lonely", 'subscriptions' => 0],
];
$this->assertResult($exp, Arsse::$db->tagList("john.doe@example.com"));
$exp = [
['id' => 3, 'name' => "Boring"],
['id' => 3, 'name' => "Boring", 'subscriptions' => 0],
];
$this->assertResult($exp, Arsse::$db->tagList("jane.doe@example.com"));
$exp = [];

20
tests/cases/Database/SeriesToken.php

@ -10,6 +10,8 @@ namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Arsse;
trait SeriesToken {
protected static $drv;
protected function setUpSeriesToken(): void {
// set up the test data
$past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute"));
@ -18,25 +20,15 @@ trait SeriesToken {
$old = gmdate("Y-m-d H:i:s", strtotime("now - 2 days"));
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["jane.doe@example.com", "",1],
["john.doe@example.com", "",2],
],
],
'arsse_tokens' => [
'columns' => [
'id' => "str",
'class' => "str",
'user' => "str",
'expires' => "datetime",
'data' => "str",
],
'rows' => [
'columns' => ["id", "class", "user", "expires", "data"],
'rows' => [
["80fa94c1a11f11e78667001e673b2560", "fever.login", "jane.doe@example.com", $faroff, null],
["27c6de8da13311e78667001e673b2560", "fever.login", "jane.doe@example.com", $past, null], // expired
["ab3b3eb8a13311e78667001e673b2560", "class.class", "jane.doe@example.com", null, null],

19
tests/cases/Database/SeriesUser.php

@ -10,28 +10,21 @@ namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Arsse;
trait SeriesUser {
protected static $drv;
protected function setUpSeriesUser(): void {
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
'admin' => 'bool',
],
'rows' => [
'columns' => ["id", "password", "num", "admin"],
'rows' => [
["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', 1, 1], // password is hash of "secret"
["jane.doe@example.com", "", 2, 0],
["john.doe@example.com", "", 3, 0],
],
],
'arsse_user_meta' => [
'columns' => [
'owner' => "str",
'key' => "str",
'value' => "str",
],
'rows' => [
'columns' => ["owner", "key", "value"],
'rows' => [
["admin@example.net", "lang", "en"],
["admin@example.net", "tz", "America/Toronto"],
["admin@example.net", "sort_asc", "0"],

3
tests/cases/Database/TestDatabase.php

@ -11,6 +11,7 @@ use JKingWeb\Arsse\Database;
/** @covers \JKingWeb\Arsse\Database */
class TestDatabase extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $drv;
protected $db = null;
public function setUp(): void {
@ -56,7 +57,7 @@ class TestDatabase extends \JKingWeb\Arsse\Test\AbstractTest {
["?,?", [null, null], [null, null], "str"],
["null", [], array_fill(0, $l, null), "str"],
["$intList", [], $ints, "int"],
["$intList,".($l + 1), [], array_merge($ints, [$l + 1]), "int"],
["$intList,".($l + 1), [], array_merge($ints, [$l + 1]), "int"],
["$intList,0", [], array_merge($ints, ["OOK"]), "int"],
["$intList", [], array_merge($ints, [null]), "int"],
["$stringList,''", [], array_merge($strings, [""]), "str"],

2
tests/cases/Db/BaseDriver.php

@ -385,6 +385,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
$greatest = $this->drv->sqlToken("GrEatESt");
$nocase = $this->drv->sqlToken("noCASE");
$like = $this->drv->sqlToken("liKe");
$integer = $this->drv->sqlToken("InTEGer");
$asc = $this->drv->sqlToken("asc");
$desc = $this->drv->sqlToken("desc");
$least = $this->drv->sqlToken("leASt");
@ -395,6 +396,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame("Z", $this->drv->query("SELECT $greatest('Z', 'A')")->getValue());
$this->assertSame("Z", $this->drv->query("SELECT 'Z' collate $nocase")->getValue());
$this->assertSame("Z", $this->drv->query("SELECT 'Z' where 'Z' $like 'z'")->getValue());
$this->assertEquals(1, $this->drv->query("SELECT CAST((1=1) as $integer)")->getValue());
$this->assertEquals([null, 1, 2], array_column($this->drv->query("SELECT 1 as t union select null as t union select 2 as t order by t $asc")->getAll(), "t"));
$this->assertEquals([2, 1, null], array_column($this->drv->query("SELECT 1 as t union select null as t union select 2 as t order by t $desc")->getAll(), "t"));
}

235
tests/cases/Db/BaseUpdate.php

@ -12,7 +12,7 @@ use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Db\Exception;
use org\bovigo\vfs\vfsStream;
class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
abstract class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $interface;
protected $drv;
protected $vfs;
@ -131,6 +131,7 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
$this->drv->schemaUpdate(-1, $this->base);
}
/** @depends testPerformActualUpdate */
public function testPerformMaintenance(): void {
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
$this->assertTrue($this->drv->maintenance());
@ -159,34 +160,212 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
QUERY_TEXT
);
$this->drv->schemaUpdate(7);
$users = [
['id' => "a", 'password' => "xyz", 'num' => 1],
['id' => "b", 'password' => "abc", 'num' => 2],
$exp = [
'arsse_users' => [
'columns' => ["id", "password", "num"],
'rows' => [
["a", "xyz", 1],
["b", "abc", 2],
]
],
'arsse_folders' => [
'columns' => ["owner", "name"],
'rows' => [
["a", "1"],
["b", "2"],
]
],
'arsse_icons' => [
'columns' => ["id", "url"],
'rows' => [
[1, "http://example.com/icon"],
[2, "http://example.org/icon"],
]
],
'arsse_feeds' => [
'columns' => ["url", "icon"],
'rows' => [
["http://example.com/", 1],
["http://example.org/", 2],
["https://example.com/", 1],
["http://example.net/", null],
]
],
'arsse_subscriptions' => [
'columns' => ["id", "scrape"],
'rows' => [
[1,1],
[2,1],
[3,0],
[4,0],
]
]
];
$folders = [
['owner' => "a", 'name' => "1"],
['owner' => "b", 'name' => "2"],
];
$icons = [
['id' => 1, 'url' => "http://example.com/icon"],
['id' => 2, 'url' => "http://example.org/icon"],
];
$feeds = [
['url' => 'http://example.com/', 'icon' => 1],
['url' => 'http://example.org/', 'icon' => 2],
['url' => 'https://example.com/', 'icon' => 1],
['url' => 'http://example.net/', 'icon' => null],
];
$subs = [
['id' => 1, 'scrape' => 1],
['id' => 2, 'scrape' => 1],
['id' => 3, 'scrape' => 0],
['id' => 4, 'scrape' => 0],
$this->compareExpectations($this->drv, $exp);
}
public function testUpdateTo8(): void {
$this->drv->schemaUpdate(7);
$this->drv->exec(
<<<QUERY_TEXT
INSERT INTO arsse_icons(id, url) values
(4, 'https://example.org/icon'),
(12, 'https://example.net/icon');
insert into arsse_feeds(id, url, title, source, updated, modified, next_fetch, orphaned, etag, err_count, err_msg, username, password, size, icon) values
(1, 'https://example.com/rss', 'Title 1', 'https://example.com/', '2001-06-13 06:55:23', '2001-06-13 06:56:23', '2001-06-13 06:57:23', '2001-06-13 06:54:23', '"ook"', 42, 'Some error', 'johndoe', 'secret', 47, null),
-- This feed has no subscriptions, so should not be seen in the new table
(2, 'https://example.org/rss', 'Title 2', 'https://example.org/', '2001-06-14 06:55:23', '2001-06-14 06:56:23', '2001-06-14 06:57:23', '2001-06-14 06:54:23', '"eek"', 5, 'That error', 'janedoe', 'secret', 2112, 4),
(3, 'https://example.net/rss', 'Title 3', 'https://example.net/', '2001-06-15 06:55:23', '2001-06-15 06:56:23', '2001-06-15 06:57:23', '2001-06-15 06:54:23', '"ack"', 44, 'This error', '', '', 3, 12);
insert into arsse_users(id,password,num,admin) values
('a', 'xyz', 1, 0),
('b', 'abc', 2, 0),
('c', 'gfy', 5, 1);
insert into arsse_folders(id, owner, parent, name) values
(1337, 'a', null, 'ook'),
(4400, 'c', null, 'eek');
insert into arsse_subscriptions(id, owner, feed, added, modified, title, order_type, pinned, folder, keep_rule, block_rule, scrape) values
(1, 'a', 1, '2002-02-02 00:02:03', '2002-02-02 00:05:03', 'User Title', 2, 1, null, 'keep', 'block', 0),
(4, 'a', 3, '2002-02-03 00:02:03', '2002-02-03 00:05:03', 'Rosy Title', 1, 0, 1337, 'meep', 'bloop', 0),
(6, 'c', 3, '2002-02-04 00:02:03', '2002-02-04 00:05:03', null, 2, 0, 4400, null, null, 1);
insert into arsse_articles(id, feed, url, title, author, published, edited, modified, guid, url_title_hash, url_content_hash, title_content_hash, content_scraped, content) values
(1, 1, 'https://example.com/1', 'Article 1', 'John Doe', '2001-11-08 22:07:55', '2002-11-08 07:51:12', '2001-11-08 23:44:56', 'GUID1', 'UTHASH1', 'UCHASH1', 'TCHASH1', 'Scraped 1', 'Content 1'),
(2, 1, 'https://example.com/2', 'Article 2', 'Jane Doe', '2001-11-09 22:07:55', '2002-11-09 07:51:12', '2001-11-09 23:44:56', 'GUID2', 'UTHASH2', 'UCHASH2', 'TCHASH2', 'Scraped 2', 'Content 2'),
(3, 2, 'https://example.org/1', 'Article 3', 'John Doe', '2001-11-10 22:07:55', '2002-11-10 07:51:12', '2001-11-10 23:44:56', 'GUID3', 'UTHASH3', 'UCHASH3', 'TCHASH3', 'Scraped 3', 'Content 3'),
(4, 2, 'https://example.org/2', 'Article 4', 'Jane Doe', '2001-11-11 22:07:55', '2002-11-11 07:51:12', '2001-11-11 23:44:56', 'GUID4', 'UTHASH4', 'UCHASH4', 'TCHASH4', 'Scraped 4', 'Content 4'),
(5, 3, 'https://example.net/1', 'Article 5', 'Adam Doe', '2001-11-12 22:07:55', '2002-11-12 07:51:12', '2001-11-12 23:44:56', 'GUID5', 'UTHASH5', 'UCHASH5', 'TCHASH5', null, 'Content 5'),
(6, 3, 'https://example.net/2', 'Article 6', 'Evie Doe', '2001-11-13 22:07:55', '2002-11-13 07:51:12', '2001-11-13 23:44:56', 'GUID6', 'UTHASH6', 'UCHASH6', 'TCHASH6', 'Scraped 6', 'Content 6');
insert into arsse_marks(article, subscription, "read", starred, modified, note, hidden) values
(1, 1, 1, 1, '2002-11-08 00:37:22', 'Note 1', 0),
(5, 4, 1, 0, '2002-11-12 00:37:22', 'Note 5', 0),
(5, 6, 0, 1, '2002-12-12 00:37:22', '', 0),
(6, 6, 0, 0, '2002-12-13 00:37:22', 'Note 6', 1);
insert into arsse_editions(article, modified) values
(1, '2000-01-01 00:00:00'),
(1, '2000-02-01 00:00:00'),
(2, '2000-01-02 00:00:00'),
(2, '2000-02-02 00:00:00'),
(3, '2000-01-03 00:00:00'),
(3, '2000-02-03 00:00:00'),
(4, '2000-01-04 00:00:00'),
(4, '2000-02-04 00:00:00'),
(5, '2000-01-05 00:00:00'),
(5, '2000-02-05 00:00:00'),
(6, '2000-01-06 00:00:00'),
(6, '2000-02-06 00:00:00');
insert into arsse_enclosures(article, url, type) values
(2, 'http://example.com/2/enclosure', 'image/png'),
(3, 'http://example.org/3/enclosure', 'image/jpg'),
(4, 'http://example.org/4/enclosure', 'audio/aac'),
(5, 'http://example.net/5/enclosure', 'application/octet-stream');
insert into arsse_categories(article, name) values
(1, 'Sport'),
(2, 'Opinion'),
(2, 'Gourds'),
(3, 'Politics'),
(6, 'Medicine'),
(6, 'Drugs'),
(6, 'Technology');
insert into arsse_labels(id, owner, name) values
(1, 'a', 'Follow-up'),
(2, 'a', 'For Gabriel!'),
(3, 'c', 'Maple'),
(4, 'c', 'Brown sugar');
insert into arsse_label_members(label, article, subscription, assigned, modified) values
(2, 2, 1, 1, '2023-09-01 11:22:33'),
(1, 2, 1, 0, '2023-09-02 11:22:33'),
(1, 5, 4, 1, '2023-09-03 11:22:33'),
(4, 5, 6, 0, '2023-09-04 11:22:33');
QUERY_TEXT
);
$this->drv->schemaUpdate(8);
$exp = [
'arsse_users' => [
'columns' => ["id", "password", "num", "admin"],
'rows' => [
["a", "xyz", 1, 0],
["b", "abc", 2, 0],
["c", "gfy", 5, 1],
]
],
'arsse_subscriptions' => [
'columns' => ["id", "owner", "url", "feed_title", "title", "folder", "last_mod", "etag", "next_fetch", "added", "source", "updated", "err_count", "err_msg", "size", "icon", "modified", "order_type", "pinned", "scrape", "keep_rule", "block_rule", "deleted"],
'rows' => [
[1, "a", "https://example.com/rss", "Title 1", "User Title", null, "2001-06-13 06:56:23", '"ook"', "2001-06-13 06:57:23", "2002-02-02 00:02:03", "https://example.com/", "2001-06-13 06:55:23", 42, "Some error", 47, null, "2002-02-02 00:05:03", 2, 1, 0, "keep", "block", 0],
[4, "a", "https://example.net/rss", "Title 3", "Rosy Title", 1337, "2001-06-15 06:56:23", '"ack"', "2001-06-15 06:57:23", "2002-02-03 00:02:03", "https://example.net/", "2001-06-15 06:55:23", 44, "This error", 3, 12, "2002-02-03 00:05:03", 1, 0, 0, "meep", "bloop", 0],
[6, "c", "https://example.net/rss", "Title 3", null, 4400, "2001-06-15 06:56:23", '"ack"', "2001-06-15 06:57:23", "2002-02-04 00:02:03", "https://example.net/", "2001-06-15 06:55:23", 44, "This error", 3, 12, "2002-02-04 00:05:03", 2, 0, 1, null, null, 0],
]
],
'arsse_articles' => [
'columns' => ["id", "subscription", "read", "starred", "hidden", "touched", "published", "edited", "modified", "marked", "url", "title", "author", "guid", "url_title_hash", "url_content_hash", "title_content_hash", "note"],
'rows' => [
[1, 1, 1, 1, 0, 0, "2001-11-08 22:07:55", "2002-11-08 07:51:12", "2001-11-08 23:44:56", "2002-11-08 00:37:22", "https://example.com/1", "Article 1", "John Doe", "GUID1", "UTHASH1", "UCHASH1", "TCHASH1", "Note 1"],
[2, 1, 0, 0, 0, 0, "2001-11-09 22:07:55", "2002-11-09 07:51:12", "2001-11-09 23:44:56", null, "https://example.com/2", "Article 2", "Jane Doe", "GUID2", "UTHASH2", "UCHASH2", "TCHASH2", ""],
[7, 4, 1, 0, 0, 0, "2001-11-12 22:07:55", "2002-11-12 07:51:12", "2001-11-12 23:44:56", "2002-11-12 00:37:22", "https://example.net/1", "Article 5", "Adam Doe", "GUID5", "UTHASH5", "UCHASH5", "TCHASH5", "Note 5"],
[8, 6, 0, 1, 0, 0, "2001-11-12 22:07:55", "2002-11-12 07:51:12", "2001-11-12 23:44:56", "2002-12-12 00:37:22", "https://example.net/1", "Article 5", "Adam Doe", "GUID5", "UTHASH5", "UCHASH5", "TCHASH5", ""],
[9, 4, 0, 0, 0, 0, "2001-11-13 22:07:55", "2002-11-13 07:51:12", "2001-11-13 23:44:56", null, "https://example.net/2", "Article 6", "Evie Doe", "GUID6", "UTHASH6", "UCHASH6", "TCHASH6", ""],
[10, 6, 0, 0, 1, 0, "2001-11-13 22:07:55", "2002-11-13 07:51:12", "2001-11-13 23:44:56", "2002-12-13 00:37:22", "https://example.net/2", "Article 6", "Evie Doe", "GUID6", "UTHASH6", "UCHASH6", "TCHASH6", "Note 6"],
]
],
'arsse_article_contents' => [
'columns' => ["id", "content"],
'rows' => [
[1, "Content 1"],
[2, "Content 2"],
[7, "Content 5"],
[8, "Content 5"],
[9, "Content 6"],
[10, "Scraped 6"],
]
],
'arsse_editions' => [
'columns' => ["id", "article", "modified"],
'rows' => [
[1, 1, "2000-01-01 00:00:00"],
[2, 1, "2000-02-01 00:00:00"],
[3, 2, "2000-01-02 00:00:00"],
[4, 2, "2000-02-02 00:00:00"],
[13, 7, "2000-01-05 00:00:00"],
[14, 7, "2000-02-05 00:00:00"],
[15, 8, "2000-01-05 00:00:00"],
[16, 8, "2000-02-05 00:00:00"],
[17, 9, "2000-01-06 00:00:00"],
[18, 9, "2000-02-06 00:00:00"],
[19, 10, "2000-01-06 00:00:00"],
[20, 10, "2000-02-06 00:00:00"],
]
],
'arsse_enclosures' => [
'columns' => ["article", "url", "type"],
'rows' => [
[2, "http://example.com/2/enclosure", "image/png"],
[7, "http://example.net/5/enclosure", "application/octet-stream"],
[8, "http://example.net/5/enclosure", "application/octet-stream"],
]
],
'arsse_categories' => [
'columns' => ["article", "name"],
'rows' => [
[1, "Sport"],
[2, "Opinion"],
[2, "Gourds"],
[9, "Medicine"],
[9, "Drugs"],
[9, "Technology"],
[10, "Medicine"],
[10, "Drugs"],
[10, "Technology"],
]
],
'arsse_label_members' => [
'columns' => ["label", "article", "assigned", "modified"],
'rows' => [
[2, 2, 1, '2023-09-01 11:22:33'],
[1, 2, 0, '2023-09-02 11:22:33'],
[1, 7, 1, '2023-09-03 11:22:33'],
[4, 8, 0, '2023-09-04 11:22:33'],
]
]
];
$this->assertEquals($users, $this->drv->query("SELECT id, password, num from arsse_users order by id")->getAll());
$this->assertEquals($folders, $this->drv->query("SELECT owner, name from arsse_folders order by owner")->getAll());
$this->assertEquals($icons, $this->drv->query("SELECT id, url from arsse_icons order by id")->getAll());
$this->assertEquals($feeds, $this->drv->query("SELECT url, icon from arsse_feeds order by id")->getAll());
$this->assertEquals($subs, $this->drv->query("SELECT id, scrape from arsse_subscriptions order by id")->getAll());
$this->compareExpectations($this->drv, $exp);
}
}

2
tests/cases/Exception/TestException.php

@ -32,7 +32,7 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest {
*/
public function testBaseClassWithoutMessage(): void {
$this->assertException("unknown");
throw new Exception();
throw new Exception;
}
/**

8
tests/cases/Feed/TestException.php

@ -151,10 +151,10 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest {
public function providePicoFeedException() {
return [
'Failed feed discovery' => [new \PicoFeed\Reader\SubscriptionNotFoundException(), "subscriptionNotFound"],
'Unsupported format' => [new \PicoFeed\Reader\UnsupportedFeedFormatException(), "unsupportedFeedFormat"],
'Malformed XML' => [new \PicoFeed\Parser\MalformedXmlException(), "malformedXml"],
'XML entity expansion' => [new \PicoFeed\Parser\XmlEntityException(), "xmlEntity"],
'Failed feed discovery' => [new \PicoFeed\Reader\SubscriptionNotFoundException, "subscriptionNotFound"],
'Unsupported format' => [new \PicoFeed\Reader\UnsupportedFeedFormatException, "unsupportedFeedFormat"],
'Malformed XML' => [new \PicoFeed\Parser\MalformedXmlException, "malformedXml"],
'XML entity expansion' => [new \PicoFeed\Parser\XmlEntityException, "xmlEntity"],
];
}

23
tests/cases/Feed/TestFeed.php

@ -101,7 +101,6 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->feedMatchLatest->with(1, Phony::any())->returns(new Result($this->latest));
$this->dbMock->feedMatchIds->with(Phony::wildcard())->returns(new Result([]));
$this->dbMock->feedMatchIds->with(1, Phony::wildcard())->returns(new Result($this->others));
$this->dbMock->feedRulesGet->returns([]);
Arsse::$db = $this->dbMock->get();
}
@ -387,26 +386,4 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame("image/gif", $f->iconType);
$this->assertSame($d, $f->iconData);
}
public function testApplyFilterRules(): void {
$exp = [
'jack' => ['new' => [false, true, true, false, true], 'changed' => [7 => true, 47 => true, 2112 => false, 1 => true, 42 => false]],
'sam' => ['new' => [false, true, false, false, false], 'changed' => [7 => false, 47 => true, 2112 => false, 1 => false, 42 => false]],
];
$this->dbMock->feedMatchIds->returns(new Result([
// these are the sixth through tenth entries in the feed; the title hashes have been omitted for brevity
['id' => 7, 'guid' => '0f2a218c311e3d8105f1b075142a5d26dabf056ffc61abe77e96c8f071bbf4a7', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''],
['id' => 47, 'guid' => '1c19e3b9018bc246b7414ae919ddebc88d0c575129e8c4a57b84b826c00f6db5', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''],
['id' => 2112, 'guid' => '964db0b9292ad0c7a6c225f2e0966f3bda53486fae65db0310c97409974e65b8', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''],
['id' => 1, 'guid' => '436070cda5713a0d9a8fdc8652c7ab142f0550697acfd5206a16c18aee355039', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''],
['id' => 42, 'guid' => '1a731433a1904220ef26e731ada7262e1d5bcecae53e7b5df9e1f5713af6e5d3', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''],
]));
$this->dbMock->feedRulesGet->returns([
'jack' => ['keep' => "", 'block' => '`A|W|J|S`u'],
'sam' => ['keep' => "`B|T|X`u", 'block' => '`C`u'],
]);
Arsse::$db = $this->dbMock->get();
$f = new Feed(5, $this->base."Filtering/1");
$this->assertSame($exp, $f->filteredItems);
}
}

86
tests/cases/ImportExport/TestImportExport.php

@ -20,8 +20,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
protected $primed;
protected $checkTables = [
'arsse_folders' => ["id", "owner", "parent", "name"],
'arsse_feeds' => ["id", "url", "title"],
'arsse_subscriptions' => ["id", "owner", "folder", "feed", "title"],
'arsse_subscriptions' => ["id", "owner", "folder", "feed_title", "title", "url", "deleted"],
'arsse_tags' => ["id", "owner", "name"],
'arsse_tag_members' => ["tag", "subscription", "assigned"],
];
@ -44,24 +43,15 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
Arsse::$db->driverSchemaUpdate();
$this->data = [
'arsse_users' => [
'columns' => [
'id' => 'str',
'password' => 'str',
'num' => 'int',
],
'rows' => [
'columns' => ["id", "password", "num"],
'rows' => [
["john.doe@example.com", "", 1],
["jane.doe@example.com", "", 2],
],
],
'arsse_folders' => [
'columns' => [
'id' => "int",
'owner' => "str",
'parent' => "int",
'name' => "str",
],
'rows' => [
'columns' => ["id", "owner", "parent", "name"],
'rows' => [
[1, "john.doe@example.com", null, "Science"],
[2, "john.doe@example.com", 1, "Rocketry"],
[3, "john.doe@example.com", null, "Politics"],
@ -70,45 +60,20 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
[6, "john.doe@example.com", 3, "National"],
],
],
'arsse_feeds' => [
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
],
'rows' => [
[1, "http://localhost:8000/Import/nasa-jpl", "NASA JPL"],
[2, "http://localhost:8000/Import/torstar", "Toronto Star"],
[3, "http://localhost:8000/Import/ars", "Ars Technica"],
[4, "http://localhost:8000/Import/cbc", "CBC News"],
[5, "http://localhost:8000/Import/citizen", "Ottawa Citizen"],
[6, "http://localhost:8000/Import/eurogamer", "Eurogamer"],
],
],
'arsse_subscriptions' => [
'columns' => [
'id' => "int",
'owner' => "str",
'folder' => "int",
'feed' => "int",
'title' => "str",
],
'rows' => [
[1, "john.doe@example.com", 2, 1, "NASA JPL"],
[2, "john.doe@example.com", 5, 2, "Toronto Star"],
[3, "john.doe@example.com", 1, 3, "Ars Technica"],
[4, "john.doe@example.com", 6, 4, "CBC News"],
[5, "john.doe@example.com", 6, 5, "Ottawa Citizen"],
[6, "john.doe@example.com", null, 6, "Eurogamer"],
'columns' => ["id", "owner", "folder", "feed_title", "title", "url", "deleted"],
'rows' => [
[1, "john.doe@example.com", 2, "NASA JPL", "NASA JPL", "http://localhost:8000/Import/nasa-jpl", 0],
[2, "john.doe@example.com", 5, "Toronto Star", "Toronto Star", "http://localhost:8000/Import/torstar", 0],
[3, "john.doe@example.com", 1, "Ars Technica", "Ars Technica", "http://localhost:8000/Import/ars", 0],
[4, "john.doe@example.com", 6, "CBC News", "CBC News", "http://localhost:8000/Import/cbc", 0],
[5, "john.doe@example.com", 6, "Ottawa Citizen", "Ottawa Citizen", "http://localhost:8000/Import/citizen", 0],
[6, "john.doe@example.com", null, "Eurogamer", "Eurogamer", "http://localhost:8000/Import/eurogamer", 0],
],
],
'arsse_tags' => [
'columns' => [
'id' => "int",
'owner' => "str",
'name' => "str",
],
'rows' => [
'columns' => ["id", "owner", "name"],
'rows' => [
[1, "john.doe@example.com", "canada"],
[2, "john.doe@example.com", "frequent"],
[3, "john.doe@example.com", "gaming"],
@ -118,12 +83,8 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
],
],
'arsse_tag_members' => [
'columns' => [
'tag' => "int",
'subscription' => "int",
'assigned' => "bool",
],
'rows' => [
'columns' => ["tag", "subscription", "assigned"],
'rows' => [
[1, 2, 1],
[1, 4, 1],
[1, 5, 1],
@ -218,7 +179,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
$this->proc->parse->returns($in);
$this->proc->get()->import("john.doe@example.com", "", false, true);
$exp = $this->primeExpectations($this->data, $this->checkTables);
$exp['arsse_subscriptions']['rows'][3] = [4, "john.doe@example.com", null, 4, "CBC"];
$exp['arsse_subscriptions']['rows'][3] = [4, "john.doe@example.com", null, "CBC News", "CBC", "http://localhost:8000/Import/cbc", 0];
$exp['arsse_folders']['rows'][] = [7, "john.doe@example.com", null, "Nature"];
$this->compareExpectations($this->drv, $exp);
}
@ -230,8 +191,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
$this->proc->parse->returns($in);
$this->proc->get()->import("john.doe@example.com", "", false, false);
$exp = $this->primeExpectations($this->data, $this->checkTables);
$exp['arsse_feeds']['rows'][] = [7, "http://localhost:8000/Import/some-feed", "Some feed"]; // author-supplied and user-supplied titles differ
$exp['arsse_subscriptions']['rows'][] = [7, "john.doe@example.com", null, 7, "Some Feed"];
$exp['arsse_subscriptions']['rows'][] = [7, "john.doe@example.com", null, "Some feed", "Some Feed", "http://localhost:8000/Import/some-feed", 0];
$exp['arsse_tags']['rows'][] = [7, "john.doe@example.com", "cryptic"];
$exp['arsse_tag_members']['rows'][] = [2, 7, 1];
$exp['arsse_tag_members']['rows'][] = [7, 7, 1];
@ -256,10 +216,12 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
$this->proc->parse->returns($in);
$this->proc->get()->import("john.doe@example.com", "", false, true);
$exp = $this->primeExpectations($this->data, $this->checkTables);
$exp['arsse_feeds']['rows'][] = [7, "http://localhost:8000/Import/some-feed", "Some feed"]; // author-supplied and user-supplied titles differ
$exp['arsse_subscriptions']['rows'] = [[7, "john.doe@example.com", 4, 7, "Some Feed"]];
$exp['arsse_subscriptions']['rows'] = [
[7, "john.doe@example.com", 4, "Some feed", "Some Feed", "http://localhost:8000/Import/some-feed", 0],
[6, "john.doe@example.com", null, "Eurogamer", "Eurogamer", "http://localhost:8000/Import/eurogamer", 1],
];
$exp['arsse_tags']['rows'] = [[2, "john.doe@example.com", "frequent"], [7, "john.doe@example.com", "cryptic"]];
$exp['arsse_tag_members']['rows'] = [[2, 7, 1], [7, 7, 1]];
$exp['arsse_tag_members']['rows'] = [[2, 7, 1], [7, 7, 1], [2, 6, 0]];
$exp['arsse_folders']['rows'] = [[4, "john.doe@example.com", null, "Photography"]];
$this->compareExpectations($this->drv, $exp);
}

4
tests/cases/Misc/TestContext.php

@ -104,7 +104,7 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
public function testCleanIdArrayValues(): void {
$methods = ["articles", "editions", "tags", "labels", "subscriptions"];
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime, -1.0];
$out = [1, 2, 4];
$c = new Context;
foreach ($methods as $method) {
@ -114,7 +114,7 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
public function testCleanFolderIdArrayValues(): void {
$methods = ["folders", "foldersShallow"];
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime, -1.0];
$out = [1, 2, 4, 0];
$c = new Context;
foreach ($methods as $method) {

1
tests/cases/Misc/TestValueInfo.php

@ -532,6 +532,7 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest {
[$this->i("P2DT1H"), [null,true], [true, false], [(48 + 1) * 60 * 60, false], [1.0 * (48 + 1) * 60 * 60, false], ["P2DT1H", true], [[$this->i("P2DT1H")], false], [$this->i("P2DT1H"), true]],
[$this->i("PT0H"), [null,true], [true, false], [0, false], [0.0, false], ["PT0S", true], [[$this->i("PT0H")], false], [$this->i("PT0H"), true]],
[$dateDiff, [null,true], [true, false], [366 * 24 * 60 * 60, false], [1.0 * 366 * 24 * 60 * 60, false], ["P366D", true], [[$dateDiff], false], [$dateNorm, true]],
["1 year, 2 days", [null,true], [true, false], [0, false], [0.0, false], ["1 year, 2 days", true], [["1 year, 2 days"], false], [\DateInterval::createFromDateString("1 year, 2 days"), false]],
["P1Y2D", [null,true], [true, false], [0, false], [0.0, false], ["P1Y2D", true], [["P1Y2D"], false], [$this->i("P1Y2D"), true]],
] as $set) {
// shift the input value off the set

107
tests/cases/REST/Miniflux/TestV1.php

@ -91,7 +91,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
Arsse::$user->method("propertiesGet")->willReturn(['num' => 42, 'admin' => false, 'root_folder_name' => null, 'tz' => "Asia/Gaza"]);
Arsse::$user->method("begin")->willReturn($this->transaction->get());
//initialize a handler
$this->h = new V1();
$this->h = new V1;
}
protected function v($value) {
@ -554,93 +554,56 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
}
/** @dataProvider provideFeedCreations */
public function testCreateAFeed(array $in, $out1, $out2, $out3, $out4, ResponseInterface $exp): void {
if ($out1 instanceof \Exception) {
$this->dbMock->feedAdd->throws($out1);
} else {
$this->dbMock->feedAdd->returns($out1);
}
if ($out2 instanceof \Exception) {
$this->dbMock->subscriptionAdd->throws($out2);
} else {
$this->dbMock->subscriptionAdd->returns($out2);
}
if ($out3 instanceof \Exception) {
$this->dbMock->subscriptionPropertiesSet->throws($out3);
} elseif ($out4 instanceof \Exception) {
$this->dbMock->subscriptionPropertiesSet->returns($out3)->throws($out4);
public function testCreateAFeed(array $in, $out, ResponseInterface $exp): void {
if ($out instanceof \Exception) {
$this->dbMock->subscriptionAdd->throws($out);
} else {
$this->dbMock->subscriptionPropertiesSet->returns($out3)->returns($out4);
$this->dbMock->subscriptionAdd->returns($out);
}
$this->assertMessage($exp, $this->req("POST", "/feeds", $in));
$in1 = $out1 !== null;
$in2 = $out2 !== null;
$in3 = $out3 !== null;
$in4 = $out4 !== null;
if ($in1) {
$this->dbMock->feedAdd->calledWith($in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $in['crawler'] ?? false);
} else {
$this->dbMock->feedAdd->never()->called();
}
if ($in2) {
$this->dbMock->begin->calledWith();
$this->dbMock->subscriptionAdd->calledWith("john.doe@example.com", $in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $in['crawler'] ?? false);
} else {
$this->dbMock->begin->never()->called();
$this->dbMock->subscriptionAdd->never()->called();
}
if ($in3) {
if ($out) {
$props = [
'folder' => $in['category_id'] - 1,
'scrape' => $in['crawler'] ?? false,
];
$this->dbMock->subscriptionPropertiesSet->calledWith("john.doe@example.com", $out2, $props);
if (!$out3 instanceof \Exception) {
$this->transaction->commit->called();
}
} else {
$this->dbMock->subscriptionPropertiesSet->never()->called();
}
if ($in4) {
$rules = [
'keep_rule' => $in['keeplist_rules'] ?? null,
'block_rule' => $in['blocklist_rules'] ?? null,
];
$this->dbMock->subscriptionPropertiesSet->calledWith("john.doe@example.com", $out2, $rules);
$this->dbMock->subscriptionAdd->calledWith("john.doe@example.com", $in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $props);
} else {
$this->dbMock->subscriptionPropertiesSet->atMost(1)->called();
$this->dbMock->subscriptionAdd->never()->called();
}
}
public function provideFeedCreations(): iterable {
self::clearData();
return [
[['category_id' => 1], null, null, null, null, V1::respError(["MissingInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/"], null, null, null, null, V1::respError(["MissingInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, null, V1::respError(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)],
[['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "keeplist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "blocklist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, null, V1::respError("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, null, V1::respError("Fetch403", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, null, V1::respError("Fetch401", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, null, V1::respError("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, null, V1::respError("FetchFormat", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, new ExceptionInput("constraintViolation"), null, null, V1::respError("DuplicateFeed", 409)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, new ExceptionInput("idMissing"), null, V1::respError("MissingCategory", 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, true, null, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "^A"], 2112, 44, true, true, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 2112, 44, true, true, HTTP::respJson(['feed_id' => 44], 201)],
[['category_id' => 1], null, V1::respError(["MissingInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/"], null, V1::respError(["MissingInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => "1"], null, V1::respError(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)],
[['feed_url' => "Not a URL", 'category_id' => 1], null, V1::respError(["InvalidInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 0], null, V1::respError(["InvalidInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, V1::respError(["InvalidInputValue", 'field' => "keeplist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, V1::respError(["InvalidInputValue", 'field' => "blocklist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), V1::respError("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), V1::respError("Fetch403", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), V1::respError("Fetch401", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), V1::respError("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), V1::respError("FetchFormat", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new ExceptionInput("constraintViolation"), V1::respError("DuplicateFeed", 409)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new ExceptionInput("idMissing"), V1::respError("MissingCategory", 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 44, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'crawler' => true], 44, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "^A"], 44, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 44, HTTP::respJson(['feed_id' => 44], 201)],
];
}

48
tests/cases/REST/NextcloudNews/TestV1_2.php

@ -328,7 +328,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock = $this->mock(Database::class);
$this->dbMock->begin->returns($this->mock(Transaction::class));
//initialize a handler
$this->h = new V1_2();
$this->h = new V1_2;
}
protected function v($value) {
@ -638,32 +638,32 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->feedListStale->never()->called();
}
public function testUpdateAFeed(): void {
$in = [
['feedId' => 42], // valid
['feedId' => 2112], // feed does not exist
['feedId' => "ook"], // invalid ID
['feedId' => -1], // invalid ID
['feed' => 42], // invalid input
/** @dataProvider provideFeedUpdates */
public function testUpdateAFeed(array $in, int $exp): void {
$this->dbMock->subscriptionUpdate->with("ook", 42)->returns(true);
$this->dbMock->subscriptionUpdate->with("eek", 2112)->throws(new ExceptionInput("subjectMissing"));
$this->dbMock->subscriptionUpdate->with(null, $this->anything())->throws(new ExceptionInput("subjectMissing"));
$this->dbMock->subscriptionUpdate->with($this->anything(), $this->lessThan(1))->throws(new ExceptionInput("typeViolation"));
$exp = HTTP::respEmpty($exp);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in)));
}
public function provideFeedUpdates(): iterable {
return [
'Valid input' => [['userId' => "ook", 'feedId' => 42], 204],
'Missing feed' => [['userId' => "eek", 'feedId' => 2112], 404],
'String ID' => [['userId' => "ook", 'feedId' => "ook"], 422],
'Negative ID' => [['userId' => "ook", 'feedId' => -1], 422],
'Bad input 1' => [['userId' => "ook", 'feed' => 42], 422],
'Bad input 2' => [['user' => "ook", 'feedId' => 42], 404],
];
$this->dbMock->feedUpdate->with(42)->returns(true);
$this->dbMock->feedUpdate->with(2112)->throws(new ExceptionInput("subjectMissing"));
$this->dbMock->feedUpdate->with($this->lessThan(1))->throws(new ExceptionInput("typeViolation"));
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[0])));
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[1])));
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[2])));
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[3])));
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[4])));
}
public function testUpdateAFeedWithoutAuthority(): void {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", ['feedId' => 42]));
$this->dbMock->feedUpdate->never()->called();
$this->dbMock->subscriptionUpdate->never()->called();
}
/** @dataProvider provideArticleQueries */
@ -859,17 +859,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testCleanUpBeforeUpdate(): void {
$this->dbMock->feedCleanup->with()->returns(true);
$this->dbMock->subscriptionCleanup->with()->returns(true);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
$this->dbMock->feedCleanup->calledWith();
$this->dbMock->subscriptionCleanup->calledWith();
}
public function testCleanUpBeforeUpdateWithoutAuthority(): void {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
$this->dbMock->feedCleanup->never()->called();
$this->dbMock->subscriptionCleanup->never()->called();
}
public function testCleanUpAfterUpdate(): void {
@ -883,7 +883,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/cleanup/after-update"));
$this->dbMock->feedCleanup->never()->called();
$this->dbMock->subscriptionCleanup->never()->called();
}
public function testQueryTheUserStatus(): void {

8
tests/cases/REST/TestREST.php

@ -60,7 +60,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideAuthenticableRequests */
public function testAuthenticateRequests(array $serverParams, array $expAttr): void {
$r = new REST();
$r = new REST;
// create a mock user manager
$this->userMock = $this->mock(User::class);
$this->userMock->auth->returns(false);
@ -94,7 +94,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
public function testSendAuthenticationChallenges(): void {
self::setConf();
$r = new REST();
$r = new REST;
$in = HTTP::respEmpty(401);
$exp = $in->withHeader("WWW-Authenticate", 'Basic realm="OOK", charset="UTF-8"');
$act = $r->challenge($in, "OOK");
@ -106,7 +106,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideUnnormalizedOrigins */
public function testNormalizeOrigins(string $origin, string $exp, array $ports = null): void {
$r = new REST();
$r = new REST;
$act = $r->corsNormalizeOrigin($origin, $ports);
$this->assertSame($exp, $act);
}
@ -187,7 +187,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideCorsHeaders */
public function testAddCorsHeaders(string $reqMethod, array $reqHeaders, array $resHeaders, array $expHeaders): void {
$r = new REST();
$r = new REST;
$req = new Request($reqMethod, "php://memory", $reqHeaders);
$res = HTTP::respEmpty(204, $resHeaders);
$exp = HTTP::respEmpty(204, $expHeaders);

24
tests/cases/REST/TinyTinyRSS/TestAPI.php

@ -147,7 +147,7 @@ LONG_STRING;
'expires' => "2112-12-21 21:12:00",
'user' => $this->userId,
]);
$this->h = new API();
$this->h = new API;
}
protected function req($data, string $method = "POST", string $target = "", string $strData = null, string $user = null): ResponseInterface {
@ -842,30 +842,24 @@ LONG_STRING;
}
/** @dataProvider provideFeedUpdates */
public function testUpdateAFeed(array $in, ?array $data, $out, ?int $id, ResponseInterface $exp): void {
public function testUpdateAFeed(array $in, ?array $data, $out, ResponseInterface $exp): void {
$in = array_merge(['op' => "updateFeed", 'sid' => "PriestsOfSyrinx"], $in);
$action = ($out instanceof \Exception) ? "throws" : "returns";
$this->dbMock->subscriptionPropertiesGet->$action($out);
$this->dbMock->feedUpdate->returns(true);
$this->dbMock->subscriptionUpdate->$action($out);
$this->assertMessage($exp, $this->req($in));
if ($data !== null) {
$this->dbMock->subscriptionPropertiesGet->calledWith(...$data);
$this->dbMock->subscriptionUpdate->calledWith(...$data);
} else {
$this->dbMock->subscriptionPropertiesGet->never()->called();
}
if ($id !== null) {
$this->dbMock->feedUpdate->calledWith($id);
} else {
$this->dbMock->feedUpdate->never()->called();
$this->dbMock->subscriptionUpdate->never()->called();
}
}
public function provideFeedUpdates(): iterable {
return [
[['feed_id' => 1], [$this->userId, 1], $this->v(['id' => 1, 'feed' => 11]), 11, $this->respGood(['status' => "OK"])],
[['feed_id' => 2], [$this->userId, 2], new ExceptionInput("subjectMissing"), null, $this->respErr("FEED_NOT_FOUND")],
[['feed_id' => -1], null, null, null, $this->respErr("INCORRECT_USAGE")],
[[], null, null, null, $this->respErr("INCORRECT_USAGE")],
[['feed_id' => 1], [$this->userId, 1], true, $this->respGood(['status' => "OK"])],
[['feed_id' => 2], [$this->userId, 2], new ExceptionInput("subjectMissing"), $this->respErr("FEED_NOT_FOUND")],
[['feed_id' => -1], null, null, $this->respErr("INCORRECT_USAGE")],
[[], null, null, $this->respErr("INCORRECT_USAGE")],
];
}

2
tests/cases/REST/TinyTinyRSS/TestIcon.php

@ -26,7 +26,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest {
Arsse::$user = $this->mock(User::class)->get();
// create a mock database interface
$this->dbMock = $this->mock(Database::class);
$this->h = new Icon();
$this->h = new Icon;
}
protected function req(string $target, string $method = "GET", string $user = null): ResponseInterface {

6
tests/cases/Service/TestSerial.php

@ -42,8 +42,8 @@ class TestSerial extends \JKingWeb\Arsse\Test\AbstractTest {
$d = new Driver;
$d->queue(1, 4, 3);
$this->assertSame(Arsse::$conf->serviceQueueWidth, $d->exec());
$this->dbMock->feedUpdate->calledWith(1);
$this->dbMock->feedUpdate->calledWith(4);
$this->dbMock->feedUpdate->calledWith(3);
$this->dbMock->subscriptionUpdate->calledWith(null, 1);
$this->dbMock->subscriptionUpdate->calledWith(null, 4);
$this->dbMock->subscriptionUpdate->calledWith(null, 3);
}
}

8
tests/cases/Service/TestService.php

@ -21,7 +21,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
self::setConf();
$this->dbMock = $this->mock(Database::class);
Arsse::$db = $this->dbMock->get();
$this->srv = new Service();
$this->srv = new Service;
}
public function testCheckIn(): void {
@ -46,7 +46,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
public function testPerformPreCleanup(): void {
$this->assertTrue(Service::cleanupPre());
$this->dbMock->feedCleanup->called();
$this->dbMock->subscriptionCleanup->called();
$this->dbMock->iconCleanup->called();
$this->dbMock->sessionCleanup->called();
}
@ -70,7 +70,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
public function testRefreshFeeds(): void {
// set up mock database actions
$this->dbMock->metaSet->returns(true);
$this->dbMock->feedCleanup->returns(true);
$this->dbMock->subscriptionCleanup->returns(true);
$this->dbMock->sessionCleanup->returns(true);
$this->dbMock->articleCleanup->returns(0);
$this->dbMock->feedListStale->returns([1,2,3]);
@ -83,7 +83,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
$d->queue->calledWith(1, 2, 3);
$d->exec->called();
$d->clean->called();
$this->dbMock->feedCleanup->called();
$this->dbMock->subscriptionCleanup->called();
$this->dbMock->iconCleanup->called();
$this->dbMock->sessionCleanup->called();
$this->dbMock->articleCleanup->called();

295
tests/lib/AbstractTest.php

@ -18,7 +18,6 @@ use JKingWeb\Arsse\Db\Driver;
use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Factory;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\Misc\URL;
use JKingWeb\Arsse\Misc\HTTP;
use Psr\Http\Message\MessageInterface;
@ -31,6 +30,142 @@ use GuzzleHttp\Psr7\ServerRequest;
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
use \DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
protected const COL_DEFS = [
'arsse_meta' => [
'key' => "str",
'value' => "str",
],
'arsse_users' => [
'id' => "str",
'password' => "str",
'num' => "int",
'admin' => "bool",
],
'arsse_user_meta' => [
'owner' => "str",
'key' => "str",
'modified' => "datetime",
'value' => "str",
],
'arsse_sessions' => [
'id' => "str",
'created' => "datetime",
'expires' => "datetime",
'user' => "str",
],
'arsse_tokens' => [
'id' => "str",
'class' => "str",
'user' => "str",
'created' => "datetime",
'expires' => "datetime",
'data' => "str",
],
'arsse_icons' => [
'id' => "int",
'url' => "str",
'modified' => "datetime",
'etag' => "str",
'next_fetch' => "datetime",
'orphaned' => "datetime",
'type' => "str",
'data' => "blob",
],
'arsse_articles' => [
'id' => "int",
'subscription' => "int",
'read' => "bool",
'starred' => "bool",
'hidden' => "bool",
'url' => "str",
'title' => "str",
'author' => "str",
'published' => "datetime",
'edited' => "datetime",
'modified' => "datetime",
'marked' => "datetime",
'guid' => "str",
'url_title_hash' => "str",
'url_content_hash' => "str",
'title_content_hash' => "str",
'note' => "str",
],
'arsse_article_contents' => [
'id' => "int",
'content' => "str",
],
'arsse_editions' => [
'id' => "int",
'article' => "int",
'modified' => "datetime",
],
'arsse_enclosures' => [
'article' => "int",
'url' => "str",
'type' => "str",
],
'arsse_categories' => [
'article' => "int",
'name' => "str",
],
'arsse_subscriptions' => [
'id' => "int",
'owner' => "str",
'url' => "str",
'feed_title' => "str",
'title' => "str",
'folder' => "int",
'last_mod' => "datetime",
'etag' => "str",
'next_fetch' => "datetime",
'added' => "datetime",
'source' => "str",
'updated' => "datetime",
'err_count' => "int",
'err_msg' => "str",
'size' => "int",
'icon' => "int",
'modified' => "datetime",
'order_type' => "int",
'pinned' => "bool",
'scrape' => "bool",
'keep_rule' => "str",
'block_rule' => "str",
'deleted' => "bool",
],
'arsse_folders' => [
'id' => "int",
'owner' => "str",
'parent' => "int",
'name' => "str",
'modified' => "datetime",
],
'arsse_tags' => [
'id' => "int",
'owner' => "str",
'name' => "str",
'modified' => "datetime",
],
'arsse_tag_members' => [
'tag' => "int",
'subscription' => "int",
'assigned' => "bool",
'modified' => "datetime",
],
'arsse_labels' => [
'id' => "int",
'owner' => "str",
'name' => "str",
'modified' => "datetime",
],
'arsse_label_members' => [
'label' => "int",
'article' => "int",
'assigned' => "bool",
'modified' => "datetime",
],
];
protected $objMock;
protected $confMock;
protected $langMock;
@ -54,7 +189,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
Arsse::$$prop = null;
}
if ($loadLang) {
Arsse::$lang = new \JKingWeb\Arsse\Lang();
Arsse::$lang = new \JKingWeb\Arsse\Lang;
}
}
@ -253,14 +388,33 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
return $value;
}
/** Inserts into the database test data in the following format:
*
* ```php
* $data = [
* 'some_table' => [
* 'columns' => ["id", "name"],
* 'rows' => [
* [1,"Dupond"],
* [2,"Dupont"],
* ]
* ],
* 'other_table' => [
* ...
* ]
* ];
* ```
*/
public function primeDatabase(Driver $drv, array $data): bool {
$tr = $drv->begin();
foreach ($data as $table => $info) {
$cols = array_map(function($v) {
return '"'.str_replace('"', '""', $v).'"';
}, array_keys($info['columns']));
}, $info['columns']);
$cols = implode(",", $cols);
$bindings = array_values($info['columns']);
$bindings = array_map(function($c) use ($table) {
return self::COL_DEFS[$table][$c];
}, $info['columns']);
$params = implode(",", array_fill(0, sizeof($info['columns']), "?"));
$s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings);
foreach ($info['rows'] as $row) {
@ -272,70 +426,104 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
return true;
}
public function compareExpectations(Driver $drv, array $expected): bool {
public function compareExpectations(Driver $drv, array $expected): void {
foreach ($expected as $table => $info) {
$cols = array_map(function($v) {
// serialize the rows of the expected output
$exp = [];
$dates = [];
foreach ($info['rows'] as $r) {
$row = [];
foreach ($r as $c => $v) {
// store any date values for later comparison
if (is_string($v) && preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/", $v)) {
$dates[] = $v;
}
// serialize to CSV, null being represented by no value
if ($v === null) {
$row[] = "";
} elseif ($drv->stringOutput() || is_string($v)) {
$row[] = '"'.str_replace('"', '""', (string) $v).'"';
} else {
$row[] = (string) $v;
}
}
$exp[] = implode(",", $row);
}
// serialize the rows of the actual output
$cols = implode(",", array_map(function($v) {
return '"'.str_replace('"', '""', $v).'"';
}, array_keys($info['columns']));
$cols = implode(",", $cols);
$types = $info['columns'];
}, $info['columns']));
$data = $drv->prepare("SELECT $cols from $table")->run()->getAll();
$cols = array_keys($info['columns']);
foreach ($info['rows'] as $index => $row) {
$this->assertCount(sizeof($cols), $row, "The number of columns in array index $index of expectations for table $table does not match its definition");
$row = array_combine($cols, $row);
foreach ($data as $index => $test) {
foreach ($test as $col => $value) {
switch ($types[$col]) {
case "datetime":
$test[$col] = $this->approximateTime($row[$col], $value);
break;
case "int":
$test[$col] = ValueInfo::normalize($value, ValueInfo::T_INT | ValueInfo::M_DROP | valueInfo::M_NULL);
break;
case "float":
$test[$col] = ValueInfo::normalize($value, ValueInfo::T_FLOAT | ValueInfo::M_DROP | valueInfo::M_NULL);
break;
case "bool":
$test[$col] = (int) ValueInfo::normalize($value, ValueInfo::T_BOOL | ValueInfo::M_DROP | valueInfo::M_NULL);
break;
$act = [];
$extra = [];
foreach ($data as $r) {
$row = [];
foreach ($r as $c => $v) {
// account for dates which might be off by one second
if (is_string($v) && preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/", $v)) {
if (array_search($v, $dates, true) === false) {
$v = Date::transform(Date::sub("PT1S", $v), "sql");
if (array_search($v, $dates, true) === false) {
$v = Date::transform(Date::add("PT2S", $v), "sql");
if (array_search($v, $dates, true) === false) {
$v = Date::transform(Date::sub("PT1S", $v), "sql");
}
}
}
}
if ($row === $test) {
$data[$index] = $test;
break;
if ($v === null) {
$row[] = "";
} elseif (is_string($v)) {
$row[] = '"'.str_replace('"', '""', (string) $v).'"';
} else {
$row[] = (string) $v;
}
}
$this->assertContains($row, $data, "Actual Table $table does not contain record at expected array index $index");
$found = array_search($row, $data, true);
unset($data[$found]);
$row = implode(",", $row);
// now search for the actual output row in the expected output
$found = array_keys($exp, $row, true);
foreach ($found as $k) {
if (!isset($act[$k])) {
$act[$k] = $row;
// skip to the next row
continue 2;
}
}
// if the row was not found, add it to a buffer which will be added to the actual output once all found rows are processed
$extra[] = $row;
}
$this->assertSame([], $data, "Actual table $table contains extra rows not in expectations");
// add any unfound rows to the end of the actual array
$base = sizeof($exp);
foreach ($extra as $k => $v) {
$act[$base + $k] = $v;
}
// sort the actual output by keys
ksort($act);
// finally perform the comparison to be shown to the tester
$this->assertSame($exp, $act, "Actual table $table does not match expectations");
}
return true;
}
public function primeExpectations(array $source, array $tableSpecs): array {
$out = [];
foreach ($tableSpecs as $table => $columns) {
// make sure the source has the table we want
$this->assertArrayHasKey($table, $source, "Source for expectations does not contain requested table $table.");
if (!isset($source[$table])) {
throw new \Exception("Source for expectations does not contain requested table $table.");
}
// fill the output, particularly the correct number of (empty) rows
$rows = sizeof($source[$table]['rows']);
$out[$table] = [
'columns' => [],
'rows' => array_fill(0, sizeof($source[$table]['rows']), []),
'columns' => $columns,
'rows' => array_fill(0, $rows, []),
];
// make sure the source has all the columns we want for the table
$cols = array_flip($columns);
$cols = array_intersect_key($cols, $source[$table]['columns']);
$this->assertSame(array_keys($cols), $columns, "Source for table $table does not contain all requested columns");
// get a map of source value offsets and keys
$targets = array_flip(array_keys($source[$table]['columns']));
foreach ($cols as $key => $order) {
// fill the column-spec
$out[$table]['columns'][$key] = $source[$table]['columns'][$key];
foreach ($source[$table]['rows'] as $index => $row) {
// fill each row column-wise with re-ordered values
$out[$table]['rows'][$index][$order] = $row[$targets[$key]];
// fill the rows with the requested data, column-wise
foreach ($columns as $c) {
if (($index = array_search($c, $source[$table]['columns'], true)) === false) {
throw new \Exception("Expected column $table.$c is not present in test data");
}
for ($a = 0; $a < $rows; $a++) {
$out[$table]['rows'][$a][] = $source[$table]['rows'][$a][$index];
}
}
}
@ -347,12 +535,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
// stringify our expectations if necessary
if (static::$stringOutput ?? false) {
$expected = $this->stringify($expected);
// MySQL is extra-special and mixes strings and integers, so we cast the data, too
if ((static::$implementation ?? "") === "MySQL") {
$data = $this->stringify($data);
}
}
$this->assertCount(sizeof($expected), $data, "Number of result rows (".sizeof($data).") differs from number of expected rows (".sizeof($expected).")");
if (sizeof($expected)) {
// make sure the expectations are consistent
foreach ($expected as $exp) {

4
tests/lib/DatabaseDrivers/MySQL.php

@ -17,7 +17,7 @@ trait MySQL {
protected static $dbResultClass = \JKingWeb\Arsse\Db\MySQL\Result::class;
protected static $dbStatementClass = \JKingWeb\Arsse\Db\MySQL\Statement::class;
protected static $dbDriverClass = \JKingWeb\Arsse\Db\MySQL\Driver::class;
protected static $stringOutput = true;
protected static $stringOutput = false;
public static function dbInterface() {
if (!class_exists("mysqli")) {
@ -26,7 +26,7 @@ trait MySQL {
$drv = new \mysqli_driver;
$drv->report_mode = \MYSQLI_REPORT_OFF;
$d = mysqli_init();
$d->options(\MYSQLI_OPT_INT_AND_FLOAT_NATIVE, false);
$d->options(\MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
$d->options(\MYSQLI_SET_CHARSET_NAME, "utf8mb4");
@$d->real_connect(Arsse::$conf->dbMySQLHost, Arsse::$conf->dbMySQLUser, Arsse::$conf->dbMySQLPass, Arsse::$conf->dbMySQLDb, Arsse::$conf->dbMySQLPort);
if ($d->connect_errno) {

4
tests/lib/DatabaseDrivers/MySQLPDO.php

@ -17,7 +17,7 @@ trait MySQLPDO {
protected static $dbResultClass = \JKingWeb\Arsse\Db\PDOResult::class;
protected static $dbStatementClass = \JKingWeb\Arsse\Db\MySQL\PDOStatement::class;
protected static $dbDriverClass = \JKingWeb\Arsse\Db\MySQL\PDODriver::class;
protected static $stringOutput = true;
protected static $stringOutput = false;
public static function dbInterface() {
try {
@ -34,7 +34,7 @@ trait MySQLPDO {
$dsn = "mysql:".implode(";", $dsn);
$d = new \PDO($dsn, Arsse::$conf->dbMySQLUser, Arsse::$conf->dbMySQLPass, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_STRINGIFY_FETCHES => true,
\PDO::ATTR_STRINGIFY_FETCHES => false,
\PDO::MYSQL_ATTR_MULTI_STATEMENTS => false,
]);
foreach (\JKingWeb\Arsse\Db\MySQL\PDODriver::makeSetupQueries() as $q) {

Loading…
Cancel
Save