Browse Source

Eliminated passing of RuntimeData instances

- RuntimeData has now been replaced by a single static Data class
- The Data class has a load() method which fills the same role as the constructor of RuntimeData
- The static Lang class is now an instantiable class and is a member of Data
- All tests have been adjusted and pass
- The Exception tests no longer require convoluted workarounds: a simple mock  for Data::$l suffices; Lang tests also use a mock to prevent loops now instead of using a workaround
microsub
J. King 7 years ago
parent
commit
f902346b6c
  1. 2
      autoload.php
  2. 3
      lib/AbstractException.php
  3. 18
      lib/Data.php
  4. 43
      lib/Database.php
  5. 2
      lib/Db/Driver.php
  6. 16
      lib/Db/SQLite3/Driver.php
  7. 116
      lib/Lang.php
  8. 18
      lib/Lang/Exception.php
  9. 5
      lib/REST.php
  10. 2
      lib/REST/AbstractHandler.php
  11. 2
      lib/REST/Handler.php
  12. 3
      lib/REST/NextCloudNews/Versions.php
  13. 16
      lib/RuntimeData.php
  14. 56
      lib/User.php
  15. 4
      lib/User/Driver.php
  16. 8
      lib/User/Internal/Driver.php
  17. 8
      lib/User/Internal/InternalFunctions.php
  18. 8
      tests/Conf/TestConf.php
  19. 34
      tests/Db/SQLite3/TestDbDriverSQLite3.php
  20. 10
      tests/Db/SQLite3/TestDbUpdateSQLite3.php
  21. 20
      tests/Exception/TestException.php
  22. 39
      tests/Lang/TestLang.php
  23. 36
      tests/Lang/TestLangErrors.php
  24. 67
      tests/Lang/testLangComplex.php
  25. 9
      tests/REST/NextCloudNews/TestNCNVersionDiscovery.php
  26. 119
      tests/User/TestAuthorization.php
  27. 15
      tests/User/TestUserInternalDriver.php
  28. 15
      tests/User/TestUserMockExternal.php
  29. 14
      tests/User/TestUserMockInternal.php
  30. 15
      tests/lib/Db/Tools.php
  31. 40
      tests/lib/Lang/Setup.php
  32. 13
      tests/lib/RuntimeData.php
  33. 15
      tests/lib/Tools.php
  34. 107
      tests/lib/User/CommonTests.php
  35. 30
      tests/lib/User/Database.php
  36. 13
      tests/lib/User/DriverExternalMock.php
  37. 8
      tests/lib/User/DriverInternalMock.php
  38. 1
      tests/lib/User/DriverSkeleton.php
  39. 7
      tests/phpunit.xml
  40. 19
      tests/test.php

2
autoload.php

@ -1,3 +1,3 @@
<?php
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
$data = new RuntimeData(new Conf());
Data::load(new Conf());

3
lib/AbstractException.php

@ -6,7 +6,6 @@ abstract class AbstractException extends \Exception {
const CODES = [
"Exception.uncoded" => -1,
"Exception.invalid" => 1, // this exception MUST NOT have a message string defined
"Exception.unknown" => 10000,
"Lang/Exception.defaultFileMissing" => 10101,
"Lang/Exception.fileMissing" => 10102,
@ -79,7 +78,7 @@ abstract class AbstractException extends \Exception {
$code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\", "/", $class).".$msgID";
}
$msg = Lang::msg($msg, $vars);
$msg = Data::$l->msg($msg, $vars);
}
parent::__construct($msg, $code, $e);
}

18
lib/Data.php

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
class Data {
public static $l;
public static $conf;
public static $db;
public static $user;
static function load(Conf $conf) {
static::$l = new Lang();
static::$conf = $conf;
static::$l->set($conf->lang);
static::$db = new Database();
static::$user = new User();
}
}

43
lib/Database.php

@ -18,10 +18,9 @@ class Database {
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
}
public function __construct(RuntimeData $data) {
$this->data = $data;
$this->driver = $driver = $data->conf->dbDriver;
$this->db = new $driver($data, INSTALL);
public function __construct() {
$this->driver = $driver = Data::$conf->dbDriver;
$this->db = new $driver(INSTALL);
$ver = $this->db->schemaVersion();
if(!INSTALL && $ver < self::SCHEMA_VERSION) {
$this->db->schemaUpdate(self::SCHEMA_VERSION);
@ -166,14 +165,14 @@ class Database {
}
public function userExists(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id is ?", "str")->run($user)->getValue();
}
public function userAdd(string $user, string $password = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->userExists($user)) throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = "";
if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
@ -181,33 +180,33 @@ class Database {
}
public function userRemove(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() < 1) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return true;
}
public function userList(string $domain = null): array {
if($domain !== null) {
if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
if(!Data::$user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain;
return $this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain)->getAll();
} else {
if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
if(!Data::$user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
return $this->db->prepare("SELECT id from arsse_users")->run()->getAll();
}
}
public function userPasswordGet(string $user): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return (string) $this->db->prepare("SELECT password from arsse_users where id is ?", "str")->run($user)->getValue();
}
public function userPasswordSet(string $user, string $password = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = "";
if(strlen($password > 0)) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user);
@ -215,14 +214,14 @@ class Database {
}
public function userPropertiesGet(string $user): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow();
if(!$prop) return [];
return $prop;
}
public function userPropertiesSet(string $user, array &$properties): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$valid = [ // FIXME: add future properties
"name" => "str",
];
@ -237,12 +236,12 @@ class Database {
}
public function userRightsGet(string $user): int {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (int) $this->db->prepare("SELECT rights from arsse_users where id is ?", "str")->run($user)->getValue();
}
public function userRightsSet(string $user, int $rights): bool {
if(!$this->data->user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) return false;
$this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
return true;
@ -250,7 +249,7 @@ class Database {
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int {
// If the user isn't authorized to perform this action then throw an exception.
if (!$this->data->user->authorize($user, __FUNCTION__)) {
if (!Data::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// If the user doesn't exist throw an exception.
@ -299,13 +298,13 @@ class Database {
}
public function subscriptionRemove(string $user, int $id): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("DELETE from arsse_subscriptions where id is ?", "int")->run($id)->changes();
}
public function folderAdd(string $user, array $data): int {
// If the user isn't authorized to perform this action then throw an exception.
if (!$this->data->user->authorize($user, __FUNCTION__)) {
if (!Data::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// If the user doesn't exist throw an exception.
@ -345,7 +344,7 @@ class Database {
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result {
// if the user isn't authorized to perform this action then throw an exception.
if (!$this->data->user->authorize($user, __FUNCTION__)) {
if (!Data::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// if the user doesn't exist throw an exception.
@ -487,7 +486,7 @@ class Database {
}
public function folderRemove(string $user, int $id): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
// common table expression to list all descendant folders of the target folder
$cte = "RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ";
$changes = 0;

2
lib/Db/Driver.php

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
interface Driver {
function __construct(\JKingWeb\Arsse\RuntimeData $data, bool $install = false);
function __construct(bool $install = false);
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
// returns the version of the scheme of the opened database; if uninitialized should return 0

16
lib/Db/SQLite3/Driver.php

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Lang;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
@ -15,16 +15,14 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
const SQLITE_MISMATCH = 20;
protected $db;
protected $data;
public function __construct(\JKingWeb\Arsse\RuntimeData $data, bool $install = false) {
public function __construct(bool $install = false) {
// check to make sure required extension is loaded
if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName());
$this->data = $data;
$file = $data->conf->dbSQLite3File;
$file = Data::$conf->dbSQLite3File;
// if the file exists (or we're initializing the database), try to open it
try {
$this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key);
$this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, Data::$conf->dbSQLite3Key);
} catch(\Throwable $e) {
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
if(!file_exists($file)) {
@ -57,7 +55,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
static public function driverName(): string {
return Lang::msg("Driver.Db.SQLite3.Name");
return Data::$l->msg("Driver.Db.SQLite3.Name");
}
public function schemaVersion(): int {
@ -66,10 +64,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function schemaUpdate(int $to): bool {
$ver = $this->schemaVersion();
if(!$this->data->conf->dbSQLite3AutoUpd) throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
if(!Data::$conf->dbSQLite3AutoUpd) throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
if($ver >= $to) throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
$sep = \DIRECTORY_SEPARATOR;
$path = $this->data->conf->dbSchemaBase.$sep."SQLite3".$sep;
$path = Data::$conf->dbSchemaBase.$sep."SQLite3".$sep;
$this->lock();
$this->begin();
for($a = $ver; $a < $to; $a++) {

116
lib/Lang.php

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use \Webmozart\Glob\Glob;
use Webmozart\Glob\Glob;
class Lang {
const DEFAULT = "en"; // fallback locale
@ -16,61 +16,67 @@ class Lang {
'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
];
static public $path = BASE."locale".DIRECTORY_SEPARATOR; // path to locale files; this is a public property to facilitate unit testing
static protected $requirementsMet = false; // whether the Intl extension is loaded
static protected $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default)
static protected $wanted = self::DEFAULT; // the currently requested locale
static protected $locale = ""; // the currently loaded locale
static protected $loaded = []; // the cascade of loaded locale file names
static protected $strings = self::REQUIRED; // the loaded locale strings, merged
public $path; // path to locale files; this is a public property to facilitate unit testing
static protected $requirementsMet = false; // whether the Intl extension is loaded
protected $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default)
protected $wanted = self::DEFAULT; // the currently requested locale
protected $locale = ""; // the currently loaded locale
protected $loaded = []; // the cascade of loaded locale file names
protected $strings = self::REQUIRED; // the loaded locale strings, merged
protected function __construct() {}
function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
$this->path = $path;
}
static public function set(string $locale, bool $immediate = false): string {
public function set(string $locale, bool $immediate = false): string {
// make sure the Intl extension is loaded
if(!self::$requirementsMet) self::checkRequirements();
if(!static::$requirementsMet) static::checkRequirements();
// if requesting the same locale as already wanted, just return (but load first if we've requested an immediate load)
if($locale==self::$wanted) {
if($immediate && !self::$synched) self::load();
if($locale==$this->wanted) {
if($immediate && !$this->synched) $this->load();
return $locale;
}
// if we've requested a locale other than the null locale, fetch the list of available files and find the closest match e.g. en_ca_somedialect -> en_ca
if($locale != "") {
$list = self::listFiles();
$list = $this->listFiles();
// if the default locale is unavailable, this is (for now) an error
if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
self::$wanted = self::match($locale, $list);
$this->wanted = $this->match($locale, $list);
} else {
self::$wanted = "";
$this->wanted = "";
}
self::$synched = false;
$this->synched = false;
// load right now if asked to, otherwise load later when actually required
if($immediate) self::load();
return self::$wanted;
if($immediate) $this->load();
return $this->wanted;
}
static public function get(bool $loaded = false): string {
public function get(bool $loaded = false): string {
// we can either return the wanted locale (default) or the currently loaded locale
return $loaded ? self::$locale : self::$wanted;
return $loaded ? $this->locale : $this->wanted;
}
public function dump(): array {
return $this->strings;
}
static public function dump(): array {
return self::$strings;
public function msg(string $msgID, $vars = null): string {
return $this($msgID, $vars);
}
static public function msg(string $msgID, $vars = null): string {
public function __invoke(string $msgID, $vars = null): string {
// if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead
if(!self::$synched) try {self::load();} catch(Lang\Exception $e) {
if(self::$wanted==self::DEFAULT) {
self::set("", true);
if(!$this->synched) try {$this->load();} catch(Lang\Exception $e) {
if($this->wanted==self::DEFAULT) {
$this->set("", true);
} else {
throw $e;
}
}
// if the requested message is not present in any of the currently loaded language files, throw an exception
// note that this is indicative of a programming error since the default locale should have all strings
if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
$msg = self::$strings[$msgID];
if(!array_key_exists($msgID, $this->strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
$msg = $this->strings[$msgID];
// variables fed to MessageFormatter must be contained in an array
if($vars===null) {
// even though strings not given parameters will not get formatted, we do not optimize this case away: we still want to catch invalid strings
@ -78,36 +84,36 @@ class Lang {
} else if(!is_array($vars)) {
$vars = [$vars];
}
$msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars);
if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
$msg = \MessageFormatter::formatMessage($this->locale, $msg, $vars);
if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
return $msg;
}
static public function list(string $locale = ""): array {
public function list(string $locale = ""): array {
$out = [];
$files = self::listFiles();
$files = $this->listFiles();
foreach($files as $tag) {
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
}
return $out;
}
static public function match(string $locale, array $list = null): string {
if($list===null) $list = self::listFiles();
$default = (self::$locale=="") ? self::DEFAULT : self::$locale;
public function match(string $locale, array $list = null): string {
if($list===null) $list = $this->listFiles();
$default = ($this->locale=="") ? self::DEFAULT : $this->locale;
return \Locale::lookup($list,$locale, true, $default);
}
static protected function checkRequirements(): bool {
if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
self::$requirementsMet = true;
static::$requirementsMet = true;
return true;
}
static protected function listFiles(): array {
$out = glob(self::$path."*.php");
protected function listFiles(): array {
$out = glob($this->path."*.php");
// built-in glob doesn't work with vfsStream (and this other glob doesn't seem to work with Windows paths), so we try both
if(empty($out)) $out = Glob::glob(self::$path."*.php"); // FIXME: we should just mock glob() in tests instead and make this a dev dependency
if(empty($out)) $out = Glob::glob($this->path."*.php"); // FIXME: we should just mock glob() in tests instead and make this a dev dependency
// trim the returned file paths to return just the language tag
$out = array_map(function($file) {
$file = str_replace(DIRECTORY_SEPARATOR, "/", $file);
@ -119,17 +125,17 @@ class Lang {
return $out;
}
static protected function load(): bool {
protected function load(): bool {
if(!self::$requirementsMet) self::checkRequirements();
// if we've requested no locale (""), just load the fallback strings and return
if(self::$wanted=="") {
self::$strings = self::REQUIRED;
self::$locale = self::$wanted;
self::$synched = true;
if($this->wanted=="") {
$this->strings = self::REQUIRED;
$this->locale = $this->wanted;
$this->synched = true;
return true;
}
// decompose the requested locale from specific to general, building a list of files to load
$tags = \Locale::parseLocale(self::$wanted);
$tags = \Locale::parseLocale($this->wanted);
$files = [];
while(sizeof($tags) > 0) {
$files[] = strtolower(\Locale::composeLocale($tags));
@ -142,7 +148,7 @@ class Lang {
// reduce the list of files to be loaded to the minimum necessary (e.g. if we go from "fr" to "fr_ca", we don't need to load "fr" or "en")
$files = [];
foreach($loaded as $file) {
if($file==self::$locale) break;
if($file==$this->locale) break;
$files[] = $file;
}
// if we need to load all files, start with the fallback strings
@ -151,17 +157,17 @@ class Lang {
$strings[] = self::REQUIRED;
} else {
// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
$strings[] = self::$strings;
$strings[] = $this->strings;
}
// read files in reverse order
$files = array_reverse($files);
foreach($files as $file) {
if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file);
if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
if(!file_exists($this->path."$file.php")) throw new Lang\Exception("fileMissing", $file);
if(!is_readable($this->path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
try {
// we use output buffering in case the language file is corrupted
ob_start();
$arr = (include self::$path."$file.php");
$arr = (include $this->path."$file.php");
} catch(\Throwable $e) {
$arr = null;
} finally {
@ -171,10 +177,10 @@ class Lang {
$strings[] = $arr;
}
// apply the results and return
self::$strings = call_user_func_array("array_replace_recursive", $strings);
self::$loaded = $loaded;
self::$locale = self::$wanted;
self::$synched = true;
$this->strings = call_user_func_array("array_replace_recursive", $strings);
$this->loaded = $loaded;
$this->locale = $this->wanted;
$this->synched = true;
return true;
}
}

18
lib/Lang/Exception.php

@ -3,22 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Lang;
class Exception extends \JKingWeb\Arsse\AbstractException {
static $test = false; // used during PHPUnit testing only
function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if(!self::$test) {
parent::__construct($msgID, $vars, $e);
} else {
$codeID = "Lang/Exception.$msgID";
if(!array_key_exists($codeID,self::CODES)) {
$code = -1;
$msg = "Exception.".str_replace("\\","/",parent::class).".uncoded";
$vars = $msgID;
} else {
$code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
}
\Exception::__construct($msg, $code, $e);
}
}
}

5
lib/REST.php

@ -26,8 +26,7 @@ class REST {
];
protected $data;
function __construct(RuntimeData $data) {
$this->data = $data;
function __construct() {
}
function dispatch(REST\Request $req = null): bool {
@ -35,7 +34,7 @@ class REST {
$api = $this->apiMatch($url, $this->apis);
$req->url = substr($url,strlen($this->apis[$api]['strip']));
$class = $this->apis[$api]['class'];
$drv = new $class($this->data);
$drv = new $class();
$drv->dispatch($req);
return true;
}

2
lib/REST/AbstractHandler.php

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
abstract class AbstractHandler implements Handler {
abstract function __construct(\JKingWeb\Arsse\RuntimeData $data);
abstract function __construct();
abstract function dispatch(Request $req): Response;
protected function parseURL(string $url): array {

2
lib/REST/Handler.php

@ -3,6 +3,6 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
interface Handler {
function __construct(\JKingWeb\Arsse\RuntimeData $data);
function __construct();
function dispatch(Request $req): Response;
}

3
lib/REST/NextCloudNews/Versions.php

@ -4,8 +4,7 @@ namespace JKingWeb\Arsse\REST\NextCloudNews;
use JKingWeb\Arsse\REST\Response;
class Versions extends \JKingWeb\Arsse\REST\AbstractHandler {
function __construct(\JKingWeb\Arsse\RuntimeData $data) {
// runtime data is not needed; this method is deliberately empty
function __construct() {
}
function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response {

16
lib/RuntimeData.php

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
class RuntimeData {
public $conf;
public $db;
public $user;
public function __construct(Conf $conf) {
$this->conf = $conf;
Lang::set($conf->lang);
$this->db = new Database($this);
$this->user = new User($this);
}
}

56
lib/User.php

@ -5,7 +5,6 @@ namespace JKingWeb\Arsse;
class User {
public $id = null;
protected $data;
protected $u;
protected $authz = true;
protected $authzSupported = 0;
@ -23,10 +22,9 @@ class User {
return $classes;
}
public function __construct(\JKingWeb\Arsse\RuntimeData $data) {
$this->data = $data;
$driver = $data->conf->userDriver;
$this->u = new $driver($data);
public function __construct() {
$driver = Data::$conf->userDriver;
$this->u = new $driver();
$this->authzSupported = $this->u->driverFunctions("authorize");
}
@ -42,9 +40,9 @@ class User {
// if we don't have a logged-in user, fetch credentials
if($this->id===null) $this->credentials();
// if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true;
if($affectedUser==Data::$user->id && $action != "userRightsSet") return true;
// get properties of actor if not already available
if(!sizeof($this->actor)) $this->actor = $this->propertiesGet($this->data->user->id);
if(!sizeof($this->actor)) $this->actor = $this->propertiesGet(Data::$user->id);
$rights =& $this->actor["rights"];
// if actor is a global admin, accept the request
if($rights==User\Driver::RIGHTS_GLOBAL_ADMIN) return true;
@ -53,7 +51,7 @@ class User {
// if actor is not some other sort of admin, deny the request
if(!in_array($rights,[User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN],true)) return false;
// if actor is a domain admin/manager and domains don't match, deny the request
if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
if(Data::$conf->userComposeNames && $this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
$test = "@".$this->actor["domain"];
if(substr($affectedUser,-1*strlen($test)) != $test) return false;
}
@ -79,7 +77,7 @@ class User {
}
public function credentials(): array {
if($this->data->conf->userAuthPreferHTTP) {
if(Data::$conf->userAuthPreferHTTP) {
return $this->credentialsHTTP();
} else {
return $this->credentialsForm();
@ -100,7 +98,7 @@ class User {
} else {
$out = ["user" => "", "password" => ""];
}
if($this->data->conf->userComposeNames && $out["user"] != "") {
if(Data::$conf->userComposeNames && $out["user"] != "") {
$out["user"] = $this->composeName($out["user"]);
}
$this->id = $out["user"];
@ -109,7 +107,7 @@ class User {
public function auth(string $user = null, string $password = null): bool {
if($user===null) {
if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP();
if(Data::$conf->userAuthPreferHTTP) return $this->authHTTP();
return $this->authForm();
} else {
$this->id = $user;
@ -117,7 +115,7 @@ class User {
switch($this->u->driverFunctions("auth")) {
case User\Driver::FUNC_EXTERNAL:
$out = $this->u->auth($user, $password);
if($out && !$this->data->db->userExists($user)) $this->autoProvision($user, $password);
if($out && !Data::$db->userExists($user)) $this->autoProvision($user, $password);
return $out;
case User\Driver::FUNC_INTERNAL:
return $this->u->auth($user, $password);
@ -176,7 +174,7 @@ class User {
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userExists($user);
if($out && !$this->data->db->userExists($user)) $this->autoProvision($user, "");
if($out && !Data::$db->userExists($user)) $this->autoProvision($user, "");
return $out;
case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization
@ -195,7 +193,7 @@ class User {
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$newPassword = $this->u->userAdd($user, $password);
// if there was no exception and we don't have the user in the internal database, add it
if(!$this->data->db->userExists($user)) $this->autoProvision($user, $newPassword);
if(!Data::$db->userExists($user)) $this->autoProvision($user, $newPassword);
return $newPassword;
case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization
@ -212,9 +210,9 @@ class User {
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userRemove($user);
if($out && $this->data->db->userExists($user)) {
if($out && Data::$db->userExists($user)) {
// if the user was removed and we have it in our data, remove it there
if(!$this->data->db->userExists($user)) $this->data->db->userRemove($user);
if(!Data::$db->userExists($user)) Data::$db->userRemove($user);
}
return $out;
case User\Driver::FUNC_INTERNAL:
@ -232,9 +230,9 @@ class User {
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userPasswordSet($user, $newPassword, $oldPassword);
if($this->data->db->userExists($user)) {
if(Data::$db->userExists($user)) {
// if the password change was successful and the user exists, set the internal password to the same value
$this->data->db->userPasswordSet($user, $out);
Data::$db->userPasswordSet($user, $out);
} else {
// if the user does not exists in the internal database, create it
$this->autoProvision($user, $out);
@ -251,7 +249,7 @@ class User {
public function propertiesGet(string $user): array {
// prepare default values
$domain = null;
if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1);
if(Data::$conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1);
$init = [
"id" => $user,
"name" => $user,
@ -267,7 +265,7 @@ class User {
// remove password if it is return (not exhaustive, but...)
if(array_key_exists('password', $out)) unset($out['password']);
// if the user does not exist in the internal database, add it
if(!$this->data->db->userExists($user)) $this->autoProvision($user, "", $out);
if(!Data::$db->userExists($user)) $this->autoProvision($user, "", $out);
return $out;
case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization
@ -289,9 +287,9 @@ class User {
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userPropertiesSet($user, $properties);
if($this->data->db->userExists($user)) {
if(Data::$db->userExists($user)) {
// if the property change was successful and the user exists, set the internal properties to the same values
$this->data->db->userPropertiesSet($user, $out);
Data::$db->userPropertiesSet($user, $out);
} else {
// if the user does not exists in the internal database, create it
$this->autoProvision($user, "", $out);
@ -313,7 +311,7 @@ class User {
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userRightsGet($user);
// if the user does not exist in the internal database, add it
if(!$this->data->db->userExists($user)) $this->autoProvision($user, "", null, $out);
if(!Data::$db->userExists($user)) $this->autoProvision($user, "", null, $out);
return $out;
case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization
@ -332,10 +330,10 @@ class User {
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userRightsSet($user, $level);
// if the user does not exist in the internal database, add it
if($out && $this->data->db->userExists($user)) {
if($out && Data::$db->userExists($user)) {
$authz = $this->authorizationEnabled();
$this->authorizationEnabled(false);
$this->data->db->userRightsSet($user, $level);
Data::$db->userRightsSet($user, $level);
$this->authorizationEnabled($authz);
} else if($out) {
$this->autoProvision($user, "", null, $level);
@ -367,18 +365,18 @@ class User {
$authz = $this->authorizationEnabled();
$this->authorizationEnabled(false);
// create the user
$out = $this->data->db->userAdd($user, $password);
$out = Data::$db->userAdd($user, $password);
// set the user rights
$this->data->db->userRightsSet($user, $rights);
Data::$db->userRightsSet($user, $rights);
// set the user properties...
if($properties===null) {
// if nothing is provided but the driver uses an external function, try to get the current values from the external source
try {
if($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) $this->data->db->userPropertiesSet($user, $this->u->userPropertiesGet($user));
if($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) Data::$db->userPropertiesSet($user, $this->u->userPropertiesGet($user));
} catch(\Throwable $e) {}
} else {
// otherwise if values are provided, use those
$this->data->db->userPropertiesSet($user, $properties);
Data::$db->userPropertiesSet($user, $properties);
}
// re-enable authorization and return
$this->authorizationEnabled($authz);

4
lib/User/Driver.php

@ -13,8 +13,8 @@ Interface Driver {
const RIGHTS_GLOBAL_MANAGER = 75; // able to act for any normal users on any domain; cannot elevate other users
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
// returns an instance of a class implementing this interface. Implemented as a static method for consistency with database classes
function __construct(\JKingWeb\Arsse\RuntimeData $data);
// returns an instance of a class implementing this interface.
function __construct();
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
// returns an array (or single queried member of same) of methods defined by this interface and whether the class implements the internal function or a custom version

8
lib/User/Internal/Driver.php

@ -1,13 +1,11 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\User\Internal;
use JKingWeb\Arsse\Lang;
use JKingWeb\Arsse\User\Driver as Iface;
final class Driver implements Iface {
use InternalFunctions;
protected $data;
protected $db;
protected $functions = [
"auth" => Iface::FUNC_INTERNAL,
@ -22,12 +20,8 @@ final class Driver implements Iface {
"userRightsSet" => Iface::FUNC_INTERNAL,
];
static public function create(\JKingWeb\Arsse\RuntimeData $data): Driver {
return new static($data);
}
static public function driverName(): string {
return Lang::msg("Driver.User.Internal.Name");
return Data::$l->msg("Driver.User.Internal.Name");
}
public function driverFunctions(string $function = null) {

8
lib/User/Internal/InternalFunctions.php

@ -1,17 +1,17 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\User\Internal;
use JKingWeb\Arsse\Data;
trait InternalFunctions {
protected $actor = [];
public function __construct(\JKingWeb\Arsse\RuntimeData $data) {
$this->data = $data;
$this->db = $this->data->db;
public function __construct() {
$this->db = Data::$db;
}
function auth(string $user, string $password): bool {
if(!$this->data->user->exists($user)) return false;
if(!Data::$user->exists($user)) return false;
$hash = $this->db->userPasswordGet($user);
if($password==="" && $hash==="") return true;
return password_verify($password, $hash);

8
tests/Conf/TestConf.php

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
class TestConf extends \PHPUnit\Framework\TestCase {
@ -10,7 +10,8 @@ class TestConf extends \PHPUnit\Framework\TestCase {
static $vfs;
static $path;
static function setUpBeforeClass() {
function setUp() {
$this->clearData();
self::$vfs = vfsStream::setup("root", null, [
'confGood' => '<?php return Array("lang" => "xx");',
'confNotArray' => '<?php return 0;',
@ -24,9 +25,10 @@ class TestConf extends \PHPUnit\Framework\TestCase {
chmod(self::$path."confUnreadable", 0000);
}
static function tearDownAfterClass() {
function tearDown() {
self::$path = null;
self::$vfs = null;
$this->clearData();
}
function testLoadDefaultValues() {

34
tests/Db/SQLite3/TestDbDriverSQLite3.php

@ -10,20 +10,22 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
protected $drv;
function setUp() {
$this->clearData();
$conf = new Conf();
$conf->dbDriver = Db\SQLite3\Driver::class;
$conf->dbSQLite3File = tempnam(sys_get_temp_dir(), 'ook');
$this->data = new Test\RuntimeData($conf);
$this->drv = new Db\SQLite3\Driver($this->data, true);
Data::$conf = $conf;
$this->drv = new Db\SQLite3\Driver(true);
}
function tearDown() {
unset($this->drv);
unlink($this->data->conf->dbSQLite3File);
unlink(Data::$conf->dbSQLite3File);
$this->clearData();
}
function testFetchDriverName() {
$class = $this->data->conf->dbDriver;
$class = Data::$conf->dbDriver;
$this->assertTrue(strlen($class::driverName()) > 0);
}
@ -38,12 +40,12 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testExecMultipleStatements() {
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)"));
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->assertEquals(2112, $ch->querySingle("SELECT id from test"));
}
function testExecTimeout() {
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$ch->exec("BEGIN EXCLUSIVE TRANSACTION");
$this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -71,7 +73,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
}
function testQueryTimeout() {
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$ch->exec("BEGIN EXCLUSIVE TRANSACTION");
$this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->query("CREATE TABLE test(id integer primary key)");
@ -102,7 +104,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testBeginTransaction() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -116,7 +118,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testCommitTransaction() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -130,7 +132,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testRollbackTransaction() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -144,7 +146,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testBeginChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -159,7 +161,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testCommitChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -178,7 +180,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -199,7 +201,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testPartiallyRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -220,7 +222,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testFullyRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);
@ -238,7 +240,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testFullyCommitChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File);
$ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin();
$this->drv->query($insert);

10
tests/Db/SQLite3/TestDbUpdateSQLite3.php

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase {
@ -16,20 +16,22 @@ class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase {
const MINIMAL2 = "pragma user_version=2";
function setUp() {
$this->clearData();
$this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]);
$conf = new Conf();
$conf->dbDriver = Db\SQLite3\Driver::class;
$conf->dbSchemaBase = $this->vfs->url();
$this->base = $this->vfs->url()."/SQLite3/";
$conf->dbSQLite3File = ":memory:";
$this->data = new Test\RuntimeData($conf);
$this->drv = new Db\SQLite3\Driver($this->data, true);
Data::$conf = $conf;
$this->drv = new Db\SQLite3\Driver(true);
}
function tearDown() {
unset($this->drv);
unset($this->data);
unset($this->vfs);
$this->clearData();
}
function testLoadMissingFile() {
@ -82,7 +84,7 @@ class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase {
}
function testPerformActualUpdate() {
$this->data->conf->dbSchemaBase = (new Conf())->dbSchemaBase;
Data::$conf->dbSchemaBase = (new Conf())->dbSchemaBase;
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
}

20
tests/Exception/TestException.php

@ -6,12 +6,15 @@ namespace JKingWeb\Arsse;
class TestException extends \PHPUnit\Framework\TestCase {
use Test\Tools;
static function setUpBeforeClass() {
Lang::set("");
function setUp() {
$this->clearData(false);
$m = $this->getMockBuilder(Lang::class)->setMethods(['__invoke'])->getMock();
$m->expects($this->any())->method("__invoke")->with($this->anything(), $this->anything())->will($this->returnValue(""));
Data::$l = $m;
}
static function tearDownAfterClass() {
Lang::set(Lang::DEFAULT);
function tearDown() {
$this->clearData(true);
}
function testBaseClass() {
@ -51,14 +54,6 @@ class TestException extends \PHPUnit\Framework\TestCase {
throw new Exception("testThisExceptionMessageDoesNotExist");
}
/**
* @depends testBaseClass
*/
function testBaseClassWithMissingMessage() {
$this->assertException("stringMissing", "Lang");
throw new Exception("invalid");
}
/**
* @depends testBaseClassWithUnknownCode
*/
@ -66,5 +61,4 @@ class TestException extends \PHPUnit\Framework\TestCase {
$this->assertException("uncoded");
throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
}
}

39
tests/Lang/TestLang.php

@ -1,48 +1,47 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
class TestLang extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup;
static $vfs;
static $path;
static $files;
static $defaultPath;
public $files;
public $path;
public $l;
function testListLanguages() {
$this->assertCount(sizeof(self::$files), Lang::list("en"));
$this->assertCount(sizeof($this->files), $this->l->list("en"));
}
/**
* @depends testListLanguages
*/
function testSetLanguage() {
$this->assertEquals("en", Lang::set("en"));
$this->assertEquals("en_ca", Lang::set("en_ca"));
$this->assertEquals("de", Lang::set("de_ch"));
$this->assertEquals("en", Lang::set("en_gb_hixie"));
$this->assertEquals("en_ca", Lang::set("en_ca_jking"));
$this->assertEquals("en", Lang::set("es"));
$this->assertEquals("", Lang::set(""));
$this->assertEquals("en", $this->l->set("en"));
$this->assertEquals("en_ca", $this->l->set("en_ca"));
$this->assertEquals("de", $this->l->set("de_ch"));
$this->assertEquals("en", $this->l->set("en_gb_hixie"));
$this->assertEquals("en_ca", $this->l->set("en_ca_jking"));
$this->assertEquals("en", $this->l->set("es"));
$this->assertEquals("", $this->l->set(""));
}
/**
* @depends testSetLanguage
*/
function testLoadInternalStrings() {
$this->assertEquals("", Lang::set("", true));
$this->assertCount(sizeof(Lang::REQUIRED), Lang::dump());
$this->assertEquals("", $this->l->set("", true));
$this->assertCount(sizeof(Lang::REQUIRED), $this->l->dump());
}
/**
* @depends testLoadInternalStrings
*/
function testLoadDefaultLanguage() {
$this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true));
$str = Lang::dump();
$this->assertEquals(Lang::DEFAULT, $this->l->set(Lang::DEFAULT, true));
$str = $this->l->dump();
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str);
}
@ -51,9 +50,9 @@ class TestLang extends \PHPUnit\Framework\TestCase {
* @depends testLoadDefaultLanguage
*/
function testLoadSupplementaryLanguage() {
Lang::set(Lang::DEFAULT, true);
$this->assertEquals("ja", Lang::set("ja", true));
$str = Lang::dump();
$this->l->set(Lang::DEFAULT, true);
$this->assertEquals("ja", $this->l->set("ja", true));
$str = $this->l->dump();
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str);
$this->assertArrayHasKey('Test.absentText', $str);

36
tests/Lang/TestLangErrors.php

@ -1,66 +1,64 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
class TestLangErrors extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup;
static $vfs;
static $path;
static $files;
static $defaultPath;
public $files;
public $path;
public $l;
function setUp() {
Lang::set("", true);
function setUpSeries() {
$this->l->set("", true);
}
function testLoadEmptyFile() {
$this->assertException("fileCorrupt", "Lang");
Lang::set("fr_ca", true);
$this->l->set("fr_ca", true);
}
function testLoadFileWhichDoesNotReturnAnArray() {
$this->assertException("fileCorrupt", "Lang");
Lang::set("it", true);
$this->l->set("it", true);
}
function testLoadFileWhichIsNotPhp() {
$this->assertException("fileCorrupt", "Lang");
Lang::set("ko", true);
$this->l->set("ko", true);
}
function testLoadFileWhichIsCorrupt() {
$this->assertException("fileCorrupt", "Lang");
Lang::set("zh", true);
$this->l->set("zh", true);
}
function testLoadFileWithooutReadPermission() {
$this->assertException("fileUnreadable", "Lang");
Lang::set("ru", true);
$this->l->set("ru", true);
}
function testLoadSubtagOfMissingLanguage() {
$this->assertException("fileMissing", "Lang");
Lang::set("pt_br", true);
$this->l->set("pt_br", true);
}
function testFetchInvalidMessage() {
$this->assertException("stringInvalid", "Lang");
Lang::set("vi", true);
$txt = Lang::msg('Test.presentText');
$this->l->set("vi", true);
$txt = $this->l->msg('Test.presentText');
}
function testFetchMissingMessage() {
$this->assertException("stringMissing", "Lang");
$txt = Lang::msg('Test.absentText');
$txt = $this->l->msg('Test.absentText');
}
function testLoadMissingDefaultLanguage() {
// this should be the last test of the series
unlink(self::$path.Lang::DEFAULT.".php");
unlink($this->path.Lang::DEFAULT.".php");
$this->assertException("defaultFileMissing", "Lang");
Lang::set("fr", true);
$this->l->set("fr", true);
}
}

67
tests/Lang/testLangComplex.php

@ -1,40 +1,39 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
class TestLangComplex extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup;
static $vfs;
static $path;
static $files;
static $defaultPath;
public $files;
public $path;
public $l;
function setUp() {
Lang::set(Lang::DEFAULT, true);
function setUpSeries() {
$this->l->set(Lang::DEFAULT, true);
}
function testLazyLoad() {
Lang::set("ja");
$this->assertArrayNotHasKey('Test.absentText', Lang::dump());
$this->l->set("ja");
$this->assertArrayNotHasKey('Test.absentText', $this->l->dump());
}
/**
* @depends testLazyLoad
*/
function testGetWantedAndLoadedLocale() {
Lang::set("en", true);
Lang::set("ja");
$this->assertEquals("ja", Lang::get());
$this->assertEquals("en", Lang::get(true));
$this->l->set("en", true);
$this->l->set("ja");
$this->assertEquals("ja", $this->l->get());
$this->assertEquals("en", $this->l->get(true));
}
function testLoadCascadeOfFiles() {
Lang::set("ja", true);
$this->assertEquals("de", Lang::set("de", true));
$str = Lang::dump();
$this->l->set("ja", true);
$this->assertEquals("de", $this->l->set("de", true));
$str = $this->l->dump();
$this->assertArrayNotHasKey('Test.absentText', $str);
$this->assertEquals('und der Stein der Weisen', $str['Test.presentText']);
}
@ -43,62 +42,62 @@ class TestLangComplex extends \PHPUnit\Framework\TestCase {
* @depends testLoadCascadeOfFiles
*/
function testLoadSubtag() {
$this->assertEquals("en_ca", Lang::set("en_ca", true));
$this->assertEquals("en_ca", $this->l->set("en_ca", true));
}
function testFetchAMessage() {
Lang::set("de", true);
$this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText'));
$this->l->set("de", true);
$this->assertEquals('und der Stein der Weisen', $this->l->msg('Test.presentText'));
}
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithMissingParameters() {
Lang::set("en_ca", true);
$this->assertEquals('{0} and {1}', Lang::msg('Test.presentText'));
$this->l->set("en_ca", true);
$this->assertEquals('{0} and {1}', $this->l->msg('Test.presentText'));
}
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithSingleNumericParameter() {
Lang::set("en_ca", true);
$this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
$this->l->set("en_ca", true);
$this->assertEquals('Default language file "en" missing', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
}
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithMultipleNumericParameters() {
Lang::set("en_ca", true);
$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
$this->l->set("en_ca", true);
$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', $this->l->msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
}
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithNamedParameters() {
$this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/Arsse/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
$this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
}
/**
* @depends testFetchAMessage
*/
function testReloadDefaultStrings() {
Lang::set("de", true);
Lang::set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
$this->l->set("de", true);
$this->l->set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
}
/**
* @depends testFetchAMessage
*/
function testReloadGeneralTagAfterSubtag() {
Lang::set("en", true);
Lang::set("en_us", true);
$this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText'));
Lang::set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
$this->l->set("en", true);
$this->l->set("en_us", true);
$this->assertEquals('and the Sorcerer\'s Stone', $this->l->msg('Test.presentText'));
$this->l->set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
}
}

9
tests/REST/NextCloudNews/TestNCNVersionDiscovery.php

@ -9,13 +9,12 @@ class TestNCNVersionDiscovery extends \PHPUnit\Framework\TestCase {
use Test\Tools;
function setUp() {
$conf = new Conf();
$this->data = new Test\RuntimeData($conf);
$this->clearData();
}
function testFetchVersionList() {
$exp = new Response(200, ['apiLevels' => ['v1-2']]);
$h = new Rest\NextCloudNews\Versions($this->data);
$h = new Rest\NextCloudNews\Versions();
$req = new Request("GET", "/");
$res = $h->dispatch($req);
$this->assertEquals($exp, $res);
@ -29,7 +28,7 @@ class TestNCNVersionDiscovery extends \PHPUnit\Framework\TestCase {
function testUseIncorrectMethod() {
$exp = new Response(405);
$h = new Rest\NextCloudNews\Versions($this->data);
$h = new Rest\NextCloudNews\Versions();
$req = new Request("POST", "/");
$res = $h->dispatch($req);
$this->assertEquals($exp, $res);
@ -37,7 +36,7 @@ class TestNCNVersionDiscovery extends \PHPUnit\Framework\TestCase {
function testUseIncorrectPath() {
$exp = new Response(404);
$h = new Rest\NextCloudNews\Versions($this->data);
$h = new Rest\NextCloudNews\Versions();
$req = new Request("GET", "/ook");
$res = $h->dispatch($req);
$this->assertEquals($exp, $res);

119
tests/User/TestAuthorization.php

@ -46,53 +46,58 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
protected $data;
function setUp(string $drv = Test\User\DriverInternalMock::class, string $db = null) {
$this->clearData();
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$conf->userComposeNames = true;
$this->data = new Test\RuntimeData($conf);
Data::$conf = $conf;
if($db !== null) {
$this->data->db = new $db($this->data);
Data::$db = new $db();
}
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
Data::$user = new User();
Data::$user->authorizationEnabled(false);
foreach(self::USERS as $user => $level) {
$this->data->user->add($user, "");
$this->data->user->rightsSet($user, $level);
Data::$user->add($user, "");
Data::$user->rightsSet($user, $level);
}
$this->data->user->authorizationEnabled(true);
Data::$user->authorizationEnabled(true);
}
function tearDown() {
$this->clearData();
}
function testSelfActionLogic() {
foreach(array_keys(self::USERS) as $user) {
$this->data->user->auth($user, "");
Data::$user->auth($user, "");
// users should be able to do basic actions for themselves
$this->assertTrue($this->data->user->authorize($user, "userExists"), "User $user could not act for themselves.");
$this->assertTrue($this->data->user->authorize($user, "userRemove"), "User $user could not act for themselves.");
$this->assertTrue(Data::$user->authorize($user, "userExists"), "User $user could not act for themselves.");
$this->assertTrue(Data::$user->authorize($user, "userRemove"), "User $user could not act for themselves.");
}
}
function testRegularUserLogic() {
foreach(self::USERS as $actor => $rights) {
if($rights != User\Driver::RIGHTS_NONE) continue;
$this->data->user->auth($actor, "");
Data::$user->auth($actor, "");
foreach(array_keys(self::USERS) as $affected) {
// regular users should only be able to act for themselves
if($actor==$affected) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach(self::LEVELS as $level) {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
// they should not be able to list users
foreach(self::DOMAINS as $domain) {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
$this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
@ -101,36 +106,36 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) continue;
$actorDomain = substr($actor,strrpos($actor,"@")+1);
$this->data->user->auth($actor, "");
Data::$user->auth($actor, "");
foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
// domain managers should be able to check any user on the same domain
if($actorDomain==$affectedDomain) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should only be able to act for regular users on the same domain
if($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) {
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// and they should only be able to set their own rights to regular user
foreach(self::LEVELS as $level) {
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
}
// they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
$this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
$this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
@ -140,37 +145,37 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) continue;
$actorDomain = substr($actor,strrpos($actor,"@")+1);
$this->data->user->auth($actor, "");
Data::$user->auth($actor, "");
$allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
// domain admins should be able to check any user on the same domain
if($actorDomain==$affectedDomain) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should be able to act for any user on the same domain who is not a global manager or admin
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) {
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should be able to set rights for any user on their domain who is not a global manager or admin, up to domain admin level
foreach(self::LEVELS as $level) {
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
}
// they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
$this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
$this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
@ -180,29 +185,29 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) continue;
$actorDomain = substr($actor,strrpos($actor,"@")+1);
$this->data->user->auth($actor, "");
Data::$user->auth($actor, "");
foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
// global managers should be able to check any user
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
// they should only be able to act for regular users
if($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) {
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// and they should only be able to set their own rights to regular user
foreach(self::LEVELS as $level) {
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
}
// they should also be able to list all users
foreach(self::DOMAINS as $domain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
$this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
@ -210,17 +215,17 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
function testGlobalAdministratorLogic() {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) continue;
$this->data->user->auth($actor, "");
Data::$user->auth($actor, "");
// global admins can do anything
foreach(self::USERS as $affected => $affectedRights) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
foreach(self::LEVELS as $level) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
}
}
foreach(self::DOMAINS as $domain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
$this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
@ -228,24 +233,24 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
function testInvalidLevelLogic() {
foreach(self::USERS as $actor => $rights) {
if(in_array($rights, self::LEVELS)) continue;
$this->data->user->auth($actor, "");
Data::$user->auth($actor, "");
foreach(array_keys(self::USERS) as $affected) {
// users with unknown/invalid rights should be treated just like regular users and only be able to act for themselves
if($actor==$affected) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach(self::LEVELS as $level) {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
$this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
// they should not be able to list users
foreach(self::DOMAINS as $domain) {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
$this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
@ -264,10 +269,10 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
'list' => [],
];
// try first with a global admin (there should be no exception)
$this->data->user->auth("gadm@example.com", "");
Data::$user->auth("gadm@example.com", "");
$this->assertCount(0, $this->checkExceptions("user@example.org", $tests));
// next try with a regular user acting on another user (everything should fail)
$this->data->user->auth("user@example.com", "");
Data::$user->auth("user@example.com", "");
$this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
}
@ -286,7 +291,7 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
// list method does not take an affected user, so do not unshift for that one
if($func != "list") array_unshift($args, $user);
try {
call_user_func_array(array($this->data->user, $func), $args);
call_user_func_array(array(Data::$user, $func), $args);
} catch(User\ExceptionAuthz $e) {
$err[] = $func;
}

15
tests/User/TestUserInternalDriver.php

@ -9,18 +9,5 @@ class TestUserInternalDriver extends \PHPUnit\Framework\TestCase {
const USER1 = "john.doe@example.com";
const USER2 = "jane.doe@example.com";
protected $data;
function setUp() {
$drv = User\Internal\Driver::class;
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$this->data = new Test\RuntimeData($conf);
$this->data->db = new Test\User\Database($this->data);
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
}
public $drv = User\Internal\Driver::class;
}

15
tests/User/TestUserMockExternal.php

@ -9,18 +9,5 @@ class TestUserMockExternal extends \PHPUnit\Framework\TestCase {
const USER1 = "john.doe@example.com";
const USER2 = "jane.doe@example.com";
protected $data;
function setUp() {
$drv = Test\User\DriverExternalMock::class;
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$this->data = new Test\RuntimeData($conf);
$this->data->db = new Test\User\Database($this->data);
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
}
public $drv = Test\User\DriverExternalMock::class;
}

14
tests/User/TestUserMockInternal.php

@ -9,17 +9,9 @@ class TestUserMockInternal extends \PHPUnit\Framework\TestCase {
const USER1 = "john.doe@example.com";
const USER2 = "jane.doe@example.com";
protected $data;
public $drv = Test\User\DriverInternalMock::class;
function setUp() {
$drv = Test\User\DriverInternalMock::class;
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$this->data = new Test\RuntimeData($conf);
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
function setUpSeries() {
Data::$db = null;
}
}

15
tests/lib/Db/Tools.php

@ -3,13 +3,16 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Test\Db;
trait Tools {
function prime(\JKingWeb\Arsse\Db\Driver $drv, array $data): bool {
protected $drv;
function prime(array $data): bool {
$drv->begin();
foreach($data as $table => $info) {
$cols = implode(",", array_keys($info['columns']));
$bindings = array_values($info['columns']);
$params = implode(",", array_fill(0, sizeof($info['columns']), "?"));
$s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings);
$s = $this->drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings);
foreach($info['rows'] as $row) {
$this->assertEquals(1, $s->runArray($row)->changes());
}
@ -18,13 +21,17 @@ trait Tools {
return true;
}
function compare(\JKingWeb\Arsse\Db\Driver $drv, array $expected): bool {
function compare(array $expected): bool {
foreach($expected as $table => $info) {
$cols = implode(",", array_keys($info['columns']));
foreach($drv->prepare("SELECT $cols from $table")->run() as $num => $row) {
foreach($this->drv->prepare("SELECT $cols from $table")->run() as $num => $row) {
$row = array_values($row);
$assertSame($expected[$table]['rows'][$num], $row, "Row $num of table $table does not match expectation.");
}
}
}
function setUp() {
}
}

40
tests/lib/Lang/Setup.php

@ -1,16 +1,16 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test\Lang;
use \org\bovigo\vfs\vfsStream, \JKingWeb\Arsse\Lang;
use org\bovigo\vfs\vfsStream;
use JKingWeb\Arsse\Lang;
use JKingWeb\Arsse\Data;
trait Setup {
static function setUpBeforeClass() {
// this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
\JKingWeb\Arsse\Lang\Exception::$test = true;
function setUp() {
// test files
self::$files = [
$this->files = [
'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
@ -28,22 +28,24 @@ trait Setup {
// unreadable file
'ru.php' => '',
];
self::$vfs = vfsStream::setup("langtest", 0777, self::$files);
self::$path = self::$vfs->url()."/";
$vfs = vfsStream::setup("langtest", 0777, $this->files);
$this->path = $vfs->url()."/";
// set up a file without read access
chmod(self::$path."ru.php", 0000);
// make the Lang class use the vfs files
self::$defaultPath = Lang::$path;
Lang::$path = self::$path;
chmod($this->path."ru.php", 0000);
// make the test Lang class use the vfs files
$this->l = new Lang($this->path);
// create a mock Lang object to keep exceptions from creating loops
$this->clearData(false);
$m = $this->getMockBuilder(Lang::class)->setMethods(['__invoke'])->getMock();
$m->expects($this->any())->method("__invoke")->with($this->anything(), $this->anything())->will($this->returnValue(""));
Data::$l = $m;
// call the additional setup method if it exists
if(method_exists($this, "setUpSeries")) $this->setUpSeries();
}
static function tearDownAfterClass() {
\JKingWeb\Arsse\Lang\Exception::$test = false;
Lang::$path = self::$defaultPath;
self::$path = null;
self::$vfs = null;
self::$files = null;
Lang::set("", true);
Lang::set(Lang::DEFAULT);
function tearDown() {
$this->clearData(true);
// call the additional teardiwn method if it exists
if(method_exists($this, "tearDownSeries")) $this->tearDownSeries();
}
}

13
tests/lib/RuntimeData.php

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test;
class RuntimeData extends \JKingWeb\Arsse\RuntimeData {
public $conf;
public $db;
public $user;
public function __construct(\JKingWeb\Arsse\Conf $conf = null) {
$this->conf = $conf;
}
}

15
tests/lib/Tools.php

@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test;
use \JKingWeb\Arsse\Exception;
use JKingWeb\Arsse\Exception;
use JKingWeb\Arsse\Data;
trait Tools {
function assertException(string $msg, string $prefix = "", string $type = "Exception") {
@ -15,4 +16,16 @@ trait Tools {
$this->expectException($class);
$this->expectExceptionCode($code);
}
function clearData(bool $loadLang = true): bool {
$r = new \ReflectionClass(\JKingWeb\Arsse\Data::class);
$props = array_keys($r->getStaticProperties());
foreach($props as $prop) {
Data::$$prop = null;
}
if($loadLang) {
Data::$l = new \JKingWeb\Arsse\Lang();
}
return true;
}
}

107
tests/lib/User/CommonTests.php

@ -1,80 +1,105 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test\User;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\Conf;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\User\Driver;
trait CommonTests {
function setUp() {
$this->clearData();
$conf = new Conf();
$conf->userDriver = $this->drv;
$conf->userAuthPreferHTTP = true;
Data::$conf = $conf;
Data::$db = new Database();
Data::$user = new User();
Data::$user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
// call the additional setup method if it exists
if(method_exists($this, "setUpSeries")) $this->setUpSeries();
}
function tearDown() {
$this->clearData();
// call the additional teardiwn method if it exists
if(method_exists($this, "tearDownSeries")) $this->tearDownSeries();
}
function testListUsers() {
$this->assertCount(0,$this->data->user->list());
$this->assertCount(0,Data::$user->list());
}
function testCheckIfAUserDoesNotExist() {
$this->assertFalse($this->data->user->exists(self::USER1));
$this->assertFalse(Data::$user->exists(self::USER1));
}
function testAddAUser() {
$this->data->user->add(self::USER1, "");
$this->assertCount(1,$this->data->user->list());
Data::$user->add(self::USER1, "");
$this->assertCount(1,Data::$user->list());
}
function testCheckIfAUserDoesExist() {
$this->data->user->add(self::USER1, "");
$this->assertTrue($this->data->user->exists(self::USER1));
Data::$user->add(self::USER1, "");
$this->assertTrue(Data::$user->exists(self::USER1));
}
function testAddADuplicateUser() {
$this->data->user->add(self::USER1, "");
Data::$user->add(self::USER1, "");
$this->assertException("alreadyExists", "User");
$this->data->user->add(self::USER1, "");
Data::$user->add(self::USER1, "");
}
function testAddMultipleUsers() {
$this->data->user->add(self::USER1, "");
$this->data->user->add(self::USER2, "");
$this->assertCount(2,$this->data->user->list());
Data::$user->add(self::USER1, "");
Data::$user->add(self::USER2, "");
$this->assertCount(2,Data::$user->list());
}
function testRemoveAUser() {
$this->data->user->add(self::USER1, "");
$this->assertCount(1,$this->data->user->list());
$this->data->user->remove(self::USER1);
$this->assertCount(0,$this->data->user->list());
Data::$user->add(self::USER1, "");
$this->assertCount(1,Data::$user->list());
Data::$user->remove(self::USER1);
$this->assertCount(0,Data::$user->list());
}
function testRemoveAMissingUser() {
$this->assertException("doesNotExist", "User");
$this->data->user->remove(self::USER1);
Data::$user->remove(self::USER1);
}
function testAuthenticateAUser() {
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
$this->data->user->add(self::USER1, "secret");
$this->data->user->add(self::USER2, "");
$this->assertTrue($this->data->user->auth());
$this->assertTrue($this->data->user->auth(self::USER1, "secret"));
$this->assertFalse($this->data->user->auth(self::USER1, "superman"));
$this->assertTrue($this->data->user->auth(self::USER2, ""));
Data::$user->add(self::USER1, "secret");
Data::$user->add(self::USER2, "");
$this->assertTrue(Data::$user->auth());
$this->assertTrue(Data::$user->auth(self::USER1, "secret"));
$this->assertFalse(Data::$user->auth(self::USER1, "superman"));
$this->assertTrue(Data::$user->auth(self::USER2, ""));
}
function testChangeAPassword() {
$this->data->user->add(self::USER1, "secret");
$this->assertEquals("superman", $this->data->user->passwordSet(self::USER1, "superman"));
$this->assertTrue($this->data->user->auth(self::USER1, "superman"));
$this->assertFalse($this->data->user->auth(self::USER1, "secret"));
$this->assertEquals("", $this->data->user->passwordSet(self::USER1, ""));
$this->assertTrue($this->data->user->auth(self::USER1, ""));
$this->assertEquals($this->data->conf->userTempPasswordLength, strlen($this->data->user->passwordSet(self::USER1)));
Data::$user->add(self::USER1, "secret");
$this->assertEquals("superman", Data::$user->passwordSet(self::USER1, "superman"));
$this->assertTrue(Data::$user->auth(self::USER1, "superman"));
$this->assertFalse(Data::$user->auth(self::USER1, "secret"));
$this->assertEquals("", Data::$user->passwordSet(self::USER1, ""));
$this->assertTrue(Data::$user->auth(self::USER1, ""));
$this->assertEquals(Data::$conf->userTempPasswordLength, strlen(Data::$user->passwordSet(self::USER1)));
}
function testChangeAPasswordForAMissingUser() {
$this->assertException("doesNotExist", "User");
$this->data->user->passwordSet(self::USER1, "superman");
Data::$user->passwordSet(self::USER1, "superman");
}
function testGetThePropertiesOfAUser() {
$this->data->user->add(self::USER1, "secret");
$p = $this->data->user->propertiesGet(self::USER1);
Data::$user->add(self::USER1, "secret");
$p = Data::$user->propertiesGet(self::USER1);
$this->assertArrayHasKey('id', $p);
$this->assertArrayHasKey('name', $p);
$this->assertArrayHasKey('domain', $p);
@ -97,22 +122,22 @@ trait CommonTests {
'domain' => 'example.com',
'rights' => Driver::RIGHTS_NONE,
];
$this->data->user->add(self::USER1, "secret");
$this->data->user->propertiesSet(self::USER1, $pSet);
$p = $this->data->user->propertiesGet(self::USER1);
Data::$user->add(self::USER1, "secret");
Data::$user->propertiesSet(self::USER1, $pSet);
$p = Data::$user->propertiesGet(self::USER1);
$this->assertArraySubset($pGet, $p);
$this->assertArrayNotHasKey('password', $p);
$this->assertFalse($this->data->user->auth(self::USER1, "superman"));
$this->assertFalse(Data::$user->auth(self::USER1, "superman"));
}
function testGetTheRightsOfAUser() {
$this->data->user->add(self::USER1, "");
$this->assertEquals(Driver::RIGHTS_NONE, $this->data->user->rightsGet(self::USER1));
Data::$user->add(self::USER1, "");
$this->assertEquals(Driver::RIGHTS_NONE, Data::$user->rightsGet(self::USER1));
}
function testSetTheRightsOfAUser() {
$this->data->user->add(self::USER1, "");
$this->data->user->rightsSet(self::USER1, Driver::RIGHTS_GLOBAL_ADMIN);
$this->assertEquals(Driver::RIGHTS_GLOBAL_ADMIN, $this->data->user->rightsGet(self::USER1));
Data::$user->add(self::USER1, "");
Data::$user->rightsSet(self::USER1, Driver::RIGHTS_GLOBAL_ADMIN);
$this->assertEquals(Driver::RIGHTS_GLOBAL_ADMIN, Data::$user->rightsGet(self::USER1));
}
}

30
tests/lib/User/Database.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test\User;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\User\Driver;
use JKingWeb\Arsse\User\Exception;
use JKingWeb\Arsse\User\ExceptionAuthz;
@ -10,48 +11,47 @@ class Database extends DriverSkeleton {
public $db = [];
public function __construct(\JKingWeb\Arsse\RuntimeData $data) {
$this->data = $data;
public function __construct() {
}
function userExists(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return parent::userExists($user);
}
function userAdd(string $user, string $password = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->userExists($user)) throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userAdd($user, $password);
}
function userRemove(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return parent::userRemove($user);
}
function userList(string $domain = null): array {
if($domain===null) {
if(!$this->data->user->authorize("", __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
if(!Data::$user->authorize("", __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
return parent::userList();
} else {
$suffix = '@'.$domain;
if(!$this->data->user->authorize($suffix, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
if(!Data::$user->authorize($suffix, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
return parent::userList($domain);
}
}
function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($newPassword===null) $newPassword = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
if($newPassword===null) $newPassword = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userPasswordSet($user, $newPassword);
}
function userPropertiesGet(string $user): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
$out = parent::userPropertiesGet($user);
unset($out['password']);
@ -59,20 +59,20 @@ class Database extends DriverSkeleton {
}
function userPropertiesSet(string $user, array $properties): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
parent::userPropertiesSet($user, $properties);
return $this->userPropertiesGet($user);
}
function userRightsGet(string $user): int {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return parent::userRightsGet($user);
}
function userRightsSet(string $user, int $level): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return parent::userRightsSet($user, $level);
}
@ -80,7 +80,7 @@ class Database extends DriverSkeleton {
// specific to mock database
function userPasswordGet(string $user): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return $this->db[$user]['password'];
}

13
tests/lib/User/DriverExternalMock.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test\User;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\User\Driver;
use JKingWeb\Arsse\User\Exception;
use PasswordGenerator\Generator as PassGen;
@ -8,7 +9,6 @@ use PasswordGenerator\Generator as PassGen;
class DriverExternalMock extends DriverSkeleton implements Driver {
public $db = [];
protected $data;
protected $functions = [
"auth" => Driver::FUNC_EXTERNAL,
"userList" => Driver::FUNC_EXTERNAL,
@ -22,10 +22,6 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
"userRightsSet" => Driver::FUNC_EXTERNAL,
];
static public function create(\JKingWeb\Arsse\RuntimeData $data): Driver {
return new static($data);
}
static public function driverName(): string {
return "Mock External Driver";
}
@ -39,8 +35,7 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
}
}
public function __construct(\JKingWeb\Arsse\RuntimeData $data) {
$this->data = $data;
public function __construct() {
}
function auth(string $user, string $password): bool {
@ -56,7 +51,7 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
function userAdd(string $user, string $password = null): string {
if($this->userExists($user)) throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userAdd($user, $password);
}
@ -75,7 +70,7 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($newPassword===null) $newPassword = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
if($newPassword===null) $newPassword = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userPasswordSet($user, $newPassword);
}

8
tests/lib/User/DriverInternalMock.php

@ -6,7 +6,6 @@ use JKingWeb\Arsse\User\Driver;
class DriverInternalMock extends Database implements Driver {
public $db = [];
protected $data;
protected $functions = [
"auth" => Driver::FUNC_INTERNAL,
"userList" => Driver::FUNC_INTERNAL,
@ -20,10 +19,6 @@ class DriverInternalMock extends Database implements Driver {
"userRightsSet" => Driver::FUNC_INTERNAL,
];
static public function create(\JKingWeb\Arsse\RuntimeData $data): Driver {
return new static($data);
}
static public function driverName(): string {
return "Mock Internal Driver";
}
@ -37,8 +32,7 @@ class DriverInternalMock extends Database implements Driver {
}
}
public function __construct(\JKingWeb\Arsse\RuntimeData $data) {
$this->data = $data;
public function __construct() {
}
function auth(string $user, string $password): bool {

1
tests/lib/User/DriverSkeleton.php

@ -10,7 +10,6 @@ use PasswordGenerator\Generator as PassGen;
abstract class DriverSkeleton {
protected $db = [];
protected $data;
function userExists(string $user): bool {
return array_key_exists($user, $this->db);

7
tests/phpunit.xml

@ -10,10 +10,13 @@
beStrictAboutTestSize="true"
stopOnError="true">
<testsuite name="Localization and exceptions">
<testsuite name="Exceptions">
<file>Exception/TestException.php</file>
</testsuite>
<testsuite name="Localization">
<file>Lang/TestLang.php</file>
<file>Lang/TestLangComplex.php</file>
<file>Lang/TestException.php</file>
<file>Lang/TestLangErrors.php</file>
</testsuite>

19
tests/test.php

@ -3,6 +3,7 @@ namespace JKingWeb\Arsse;
const INSTALL = true;
require_once "../bootstrap.php";
$user = "john.doe@example.com";
$pass = "secret";
$_SERVER['PHP_AUTH_USER'] = $user;
@ -10,13 +11,13 @@ $_SERVER['PHP_AUTH_PW'] = $pass;
$conf = new Conf();
$conf->dbSQLite3File = ":memory:";
$conf->userAuthPreferHTTP = true;
$data = new RuntimeData($conf);
$data->db->schemaUpdate();
Data::load($conf);
Data::$db->schemaUpdate();
$data->user->add($user, $pass);
$data->user->auth();
$data->user->authorizationEnabled(false);
$data->user->rightsSet($user, User\Driver::RIGHTS_GLOBAL_ADMIN);
$data->user->authorizationEnabled(true);
$data->db->folderAdd($user, ['name' => 'ook']);
$data->db->subscriptionAdd($user, "http://www.tbray.org/ongoing/ongoing.atom");
Data::$user->add($user, $pass);
Data::$user->auth();
Data::$user->authorizationEnabled(false);
Data::$user->rightsSet($user, User\Driver::RIGHTS_GLOBAL_ADMIN);
Data::$user->authorizationEnabled(true);
Data::$db->folderAdd($user, ['name' => 'ook']);
Data::$db->subscriptionAdd($user, "http://www.tbray.org/ongoing/ongoing.atom");
Loading…
Cancel
Save