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

Merge branch 'master' into microsub

This commit is contained in:
J. King 2019-09-25 18:40:29 -04:00
commit 6d2b587e38
22 changed files with 215 additions and 133 deletions

View file

@ -4,6 +4,8 @@ Version 0.8.1 (2019-??-??)
Bug fixes:
- Don't crash updating feeds cached solely via ETag
- Don't fail adding a feed which collides with another via redirection
- Don't fail on very long text-search queries containing question marks
when using PostgreSQL or MySQL
Changes:
- Include a user manual

View file

@ -126,7 +126,7 @@ class RoboFile extends \Robo\Tasks {
$execpath = norm(BASE."vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit");
$confpath = realpath(BASE_TEST."phpunit.dist.xml") ?: norm(BASE_TEST."phpunit.xml");
$this->taskServer(8000)->host("localhost")->dir(BASE_TEST."docroot")->rawArg("-n")->arg(BASE_TEST."server.php")->rawArg($this->blackhole())->background()->run();
return $this->taskExec($executor)->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
}
/** Packages a given commit of the software into a release tarball

View file

@ -165,7 +165,7 @@ class Database {
// nulls are pointless to have
continue;
} elseif (is_string($v)) {
if (strlen($v) > self::LIMIT_SET_STRING_LENGTH) {
if (strlen($v) > self::LIMIT_SET_STRING_LENGTH || strpos($v, "?") !== false) {
$clause[] = "?";
$params[] = $v;
} else {
@ -198,16 +198,14 @@ class Database {
* @param boolean $matchAny Whether the search is successful when it matches any (true) or all (false) terms
*/
protected function generateSearch(array $terms, array $cols, bool $matchAny = false): array {
if (!$cols) {
throw new Exception("arrayEmpty", "cols"); // @codeCoverageIgnore
}
$clause = [];
$types = [];
$values = [];
$like = $this->db->sqlToken("like");
assert(sizeof($cols) > 0, new Exception("arrayEmpty", "cols"));
$embedSet = sizeof($terms) > ((int) (self::LIMIT_SET_SIZE / sizeof($cols)));
foreach ($terms as $term) {
$embedTerm = ($embedSet && strlen($term) <= self::LIMIT_SET_STRING_LENGTH);
$embedTerm = ($embedSet && strlen($term) <= self::LIMIT_SET_STRING_LENGTH && strpos($term, "?") === false);
$term = str_replace(["%", "_", "^"], ["^%", "^_", "^^"], $term);
$term = "%$term%";
$term = $embedTerm ? $this->db->literalString($term) : $term;
@ -221,10 +219,11 @@ class Database {
$values[] = $term;
}
}
$clause[] = "(".implode(" or ", $spec).")";
$spec = sizeof($spec) > 1 ? "(".implode(" or ", $spec).")" : (string) array_pop($spec);
$clause[] = $spec;
}
$glue = $matchAny ? "or" : "and";
$clause = $clause ? "(".implode(" $glue ", $clause).")" : "";
$clause = sizeof($clause) > 1 ? "(".implode(" $glue ", $clause).")" : (string) array_pop($clause);
return [$clause, $types, $values];
}
@ -2082,9 +2081,7 @@ class Database {
* @param boolean $byName Whether to interpret the $id parameter as the label's name (true) or identifier (false)
*/
public function labelArticlesSet(string $user, $id, Context $context, int $mode = self::ASSOC_ADD, bool $byName = false): int {
if (!in_array($mode, [self::ASSOC_ADD, self::ASSOC_REMOVE, self::ASSOC_REPLACE])) {
throw new Exception("constantUnknown", $mode); // @codeCoverageIgnore
}
assert(in_array($mode, [self::ASSOC_ADD, self::ASSOC_REMOVE, self::ASSOC_REPLACE]), new Exception("constantUnknown", $mode));
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
@ -2387,9 +2384,7 @@ class Database {
* @param boolean $byName Whether to interpret the $id parameter as the tag's name (true) or identifier (false)
*/
public function tagSubscriptionsSet(string $user, $id, array $subscriptions, int $mode = self::ASSOC_ADD, bool $byName = false): int {
if (!in_array($mode, [self::ASSOC_ADD, self::ASSOC_REMOVE, self::ASSOC_REPLACE])) {
throw new Exception("constantUnknown", $mode); // @codeCoverageIgnore
}
assert(in_array($mode, [self::ASSOC_ADD, self::ASSOC_REMOVE, self::ASSOC_REPLACE]), new Exception("constantUnknown", $mode));
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}

View file

@ -56,9 +56,7 @@ abstract class AbstractStatement implements Statement {
$this->retypeArray($binding, true);
} else {
$bindId = self::TYPES[trim(strtolower($binding))] ?? 0;
if (!$bindId) {
throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore
}
assert($bindId, new Exception("paramTypeInvalid", $binding));
$this->types[] = $bindId;
}
}

View file

@ -152,13 +152,17 @@ class URL {
* @param string $data The data to append. This should already be escaped where necessary and not start with any delimiter
* @param string $glue The query subcomponent delimiter, usually "&". If the URL has no query, "?" will be prepended instead
*/
public function queryAppend(string $url, string $data, string $glue = "&"): string {
public static function queryAppend(string $url, string $data, string $glue = "&"): string {
if (!strlen($data)) {
return $url;
}
$insPos = strpos($url, "#");
$insPos = $insPos === false ? strlen($url) : $insPos;
$hasQuery = strpos($url, "?") !== false;
$qPos = strpos($url, "?");
$hasQuery = $qPos !== false;
$glue = $hasQuery ? $glue : "?";
if ($hasQuery && $insPos > 0) {
if ($url[$insPos - 1] === $glue) {
if ($url[$insPos - 1] === $glue || ($insPos - 1) == $qPos) {
// if the URL already has excess glue, use it
$glue = "";
}

View file

@ -10,5 +10,7 @@ const NS_BASE = __NAMESPACE__."\\";
define(NS_BASE."BASE", dirname(__DIR__).DIRECTORY_SEPARATOR);
const DOCROOT = BASE."tests".DIRECTORY_SEPARATOR."docroot".DIRECTORY_SEPARATOR;
ini_set("memory_limit", "-1");
ini_set("zend.assertions", "1");
ini_set("assert.exception", "true");
error_reporting(\E_ALL);
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";

View file

@ -10,7 +10,7 @@ use JKingWeb\Arsse\Test\Database;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User;
abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest {
abstract class AbstractTest extends \JKingWeb\Arsse\Test\AbstractTest {
use SeriesMiscellany;
use SeriesMeta;
use SeriesUser;

View file

@ -0,0 +1,86 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Database;
/** @covers \JKingWeb\Arsse\Database */
class TestDatabase extends \JKingWeb\Arsse\Test\AbstractTest {
protected $db = null;
public function setUp() {
self::clearData();
self::setConf();
try {
$this->db = \Phake::makeVisible(\Phake::partialMock(Database::class));
} catch (\JKingWeb\Arsse\Db\Exception $e) {
$this->markTestSkipped("SQLite 3 database driver not available");
}
}
public function tearDown() {
$this->db = null;
self::clearData();
}
/** @dataProvider provideInClauses */
public function testGenerateInClause(string $clause, array $values, array $inV, string $inT) {
$types = array_fill(0, sizeof($values), $inT);
$exp = [$clause, $types, $values];
$this->assertSame($exp, $this->db->generateIn($inV, $inT));
}
public function provideInClauses() {
$l = Database::LIMIT_SET_SIZE + 1;
$strings = array_fill(0, $l, "");
$ints = range(1, $l);
$longString = str_repeat("0", Database::LIMIT_SET_STRING_LENGTH + 1);
$params = implode(",", array_fill(0, $l, "?"));
$intList = implode(",", $ints);
$stringList = implode(",", array_fill(0, $l, "''"));
return [
["null", [], [], "str"],
["?", [1], [1], "int"],
["?", ["1"], ["1"], "int"],
["?,?", [null, null], [null, null], "str"],
["null", [], array_fill(0, $l, null), "str"],
["$intList", [], $ints, "int"],
["$intList,".($l+1), [], array_merge($ints, [$l+1]), "int"],
["$intList,0", [], array_merge($ints, ["OOK"]), "int"],
["$intList", [], array_merge($ints, [null]), "int"],
["$stringList,''", [], array_merge($strings, [""]), "str"],
["$stringList", [], array_merge($strings, [null]), "str"],
["$stringList,?", [$longString], array_merge($strings, [$longString]), "str"],
["$stringList,'A''s'", [], array_merge($strings, ["A's"]), "str"],
["$stringList,?", ["???"], array_merge($strings, ["???"]), "str"],
["$params", $ints, $ints, "bool"],
];
}
/** @dataProvider provideSearchClauses */
public function testGenerateSearchClause(string $clause, array $values, array $inV, array $inC, bool $inAny) {
// this is not an exhaustive test; integration tests already cover the ins and outs of the functionality
$types = array_fill(0, sizeof($values), "str");
$exp = [$clause, $types, $values];
$this->assertSame($exp, $this->db->generateSearch($inV, $inC, $inAny));
}
public function provideSearchClauses() {
$terms = array_fill(0, Database::LIMIT_SET_SIZE + 1, "a");
$clause = array_fill(0, Database::LIMIT_SET_SIZE + 1, "test like '%a%' escape '^'");
$longString = str_repeat("0", Database::LIMIT_SET_STRING_LENGTH + 1);
return [
["test like ? escape '^'", ["%a%"], ["a"], ["test"], true],
["(col1 like ? escape '^' or col2 like ? escape '^')", ["%a%", "%a%"], ["a"], ["col1", "col2"], true],
["(".implode(" or ", $clause).")", [], $terms, ["test"], true],
["(".implode(" and ", $clause).")", [], $terms, ["test"], false],
["(".implode(" or ", $clause)." or test like ? escape '^')", ["%$longString%"], array_merge($terms, [$longString]), ["test"], true],
["(".implode(" or ", $clause)." or test like ? escape '^')", ["%Eh?%"], array_merge($terms, ["Eh?"]), ["test"], true],
["(".implode(" or ", $clause)." or test like ? escape '^')", ["%?%"], array_merge($terms, ["?"]), ["test"], true],
];
}
}

View file

@ -12,7 +12,7 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQL;
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQL;
protected function nextID(string $table): int {

View file

@ -13,7 +13,7 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQLPDO;
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQLPDO;
protected function nextID(string $table): int {

View file

@ -12,7 +12,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQL;
protected function nextID(string $table): int {

View file

@ -13,7 +13,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQLPDO;
protected function nextID(string $table): int {

View file

@ -11,7 +11,7 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3;
protected function nextID(string $table): int {

View file

@ -10,7 +10,7 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3PDO;
protected function nextID(string $table): int {

View file

@ -75,4 +75,20 @@ class TestURL extends \JKingWeb\Arsse\Test\AbstractTest {
[" ", "%20"],
];
}
/** @dataProvider provideQueries */
public function testAppendQueryParameters(string $url, string $query, string $exp) {
$this->assertSame($exp, URL::queryAppend($url, $query));
}
public function provideQueries() {
return [
["/", "ook=eek", "/?ook=eek"],
["/?", "ook=eek", "/?ook=eek"],
["/#ack", "ook=eek", "/?ook=eek#ack"],
["/?Huh?", "ook=eek", "/?Huh?&ook=eek"],
["/?Eh?&Huh?&", "ook=eek", "/?Eh?&Huh?&ook=eek"],
["/#ack", "", "/#ack"],
];
}
}

View file

@ -145,33 +145,11 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
return $value;
}
protected function req($dataGet, $dataPost = "", string $method = "POST", string $type = null, string $url = "", string $user = null): ServerRequest {
$url = "/fever/".$url;
protected function req($dataGet, $dataPost = "", string $method = "POST", string $type = null, string $target = "", string $user = null): ServerRequest {
$prefix = "/fever/";
$url = $prefix.$target;
$type = $type ?? "application/x-www-form-urlencoded";
$server = [
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $url,
'HTTP_CONTENT_TYPE' => $type,
];
$req = new ServerRequest($server, [], $url, $method, "php://memory", ['Content-Type' => $type]);
if (!is_array($dataGet)) {
parse_str($dataGet, $dataGet);
}
$req = $req->withRequestTarget($url)->withQueryParams($dataGet);
if (is_array($dataPost)) {
$req = $req->withParsedBody($dataPost);
} else {
parse_str($dataPost, $arr);
$req = $req->withParsedBody($arr);
}
if (isset($user)) {
if (strlen($user)) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
} else {
$req = $req->withAttribute("authenticationFailed", true);
}
}
return $req;
return $this->serverRequest($method, $url, $prefix, [], [], $dataPost, $type, $dataGet, $user);
}
public function setUp() {
@ -457,7 +435,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
return [
'Not an API request' => [$this->req(""), new EmptyResponse(404)],
'Wrong method' => [$this->req("api", "", "GET"), new EmptyResponse(405, ['Allow' => "OPTIONS,POST"])],
'Wrong content type' => [$this->req("api", "", "POST", "application/json"), new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"])],
'Wrong content type' => [$this->req("api", '{"api_key":"validToken"}', "POST", "application/json"), new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"])],
];
}

View file

@ -298,40 +298,10 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
],
];
protected function req(string $method, string $target, string $data = "", array $headers = []): ResponseInterface {
$url = "/index.php/apps/news/api/v1-2".$target;
$server = [
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $url,
'PHP_AUTH_USER' => "john.doe@example.com",
'PHP_AUTH_PW' => "secret",
'REMOTE_USER' => "john.doe@example.com",
];
if (strlen($data)) {
$server['HTTP_CONTENT_TYPE'] = "application/json";
}
$req = new ServerRequest($server, [], $url, $method, "php://memory");
if (Arsse::$user->auth("john.doe@example.com", "secret")) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", "john.doe@example.com");
}
foreach ($headers as $key => $value) {
if (!is_null($value)) {
$req = $req->withHeader($key, $value);
} else {
$req = $req->withoutHeader($key);
}
}
if (strlen($data)) {
$body = $req->getBody();
$body->write($data);
$req = $req->withBody($body);
}
$q = $req->getUri()->getQuery();
if (strlen($q)) {
parse_str($q, $q);
$req = $req->withQueryParams($q);
}
$req = $req->withRequestTarget($target);
protected function req(string $method, string $target, string $data = "", array $headers = [], bool $authenticated = true): ResponseInterface {
$prefix = "/index.php/apps/news/api/v1-2";
$url = $prefix.$target;
$req = $this->serverRequest($method, $url, $prefix, $headers, [], $data, "application/json", [], $authenticated ? "john.doe@example.com" : "");
return $this->h->dispatch($req);
}
@ -340,7 +310,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
self::setConf();
// create a mock user manager
Arsse::$user = \Phake::mock(User::class);
\Phake::when(Arsse::$user)->auth->thenReturn(true);
Arsse::$user->id = "john.doe@example.com";
// create a mock database interface
Arsse::$db = \Phake::mock(Database::class);
@ -357,9 +326,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testSendAuthenticationChallenge() {
\Phake::when(Arsse::$user)->auth->thenReturn(false);
$exp = new EmptyResponse(401);
$this->assertMessage($exp, $this->req("GET", "/"));
$this->assertMessage($exp, $this->req("GET", "/", "", [], false));
}
public function testRespondToInvalidPaths() {

View file

@ -19,13 +19,9 @@ class TestVersions extends \JKingWeb\Arsse\Test\AbstractTest {
}
protected function req(string $method, string $target): ResponseInterface {
$url = "/index.php/apps/news/api".$target;
$server = [
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $url,
];
$req = new ServerRequest($server, [], $url, $method, "php://memory");
$req = $req->withRequestTarget($target);
$prefix = "/index.php/apps/news/api";
$url = $prefix.$target;
$req = $this->serverRequest($method, $url, $prefix);
return (new Versions)->dispatch($req);
}

View file

@ -126,27 +126,10 @@ LONG_STRING;
}
protected function req($data, string $method = "POST", string $target = "", string $strData = null, string $user = null): ResponseInterface {
$url = "/tt-rss/api".$target;
$server = [
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $url,
'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded",
];
$req = new ServerRequest($server, [], $url, $method, "php://memory");
$body = $req->getBody();
if (!is_null($strData)) {
$body->write($strData);
} else {
$body->write(json_encode($data));
}
$req = $req->withBody($body)->withRequestTarget($target);
if (isset($user)) {
if (strlen($user)) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
} else {
$req = $req->withAttribute("authenticationFailed", true);
}
}
$prefix = "/tt-rss/api";
$url = $prefix.$target;
$body = $strData ?? json_encode($data);
$req = $this->serverRequest($method, $url, $prefix, [], ['HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded"], $body, "application/json", [], $user);
return $this->h->dispatch($req);
}

View file

@ -34,20 +34,9 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest {
}
protected function req(string $target, string $method = "GET", string $user = null): ResponseInterface {
$url = "/tt-rss/feed-icons/".$target;
$server = [
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $url,
];
$req = new ServerRequest($server, [], $url, $method, "php://memory");
$req = $req->withRequestTarget($target);
if (isset($user)) {
if (strlen($user)) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
} else {
$req = $req->withAttribute("authenticationFailed", true);
}
}
$prefix = "/tt-rss/feed-icons/";
$url = $prefix.$target;
$req = $this->serverRequest($method, $url, $prefix, [], [], null, "", [], $user);
return $this->h->dispatch($req);
}

View file

@ -13,10 +13,12 @@ use JKingWeb\Arsse\Db\Driver;
use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\Misc\URL;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\XmlResponse;
@ -61,6 +63,68 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
Arsse::$conf = (($force ? null : Arsse::$conf) ?? (new Conf))->import($defaults)->import($conf);
}
protected function serverRequest(string $method, string $url, string $urlPrefix, array $headers = [], array $vars = [], $body = null, string $type = "", $params = [], string $user = null): ServerRequestInterface {
$server = [
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $url,
];
if (strlen($type)) {
$server['HTTP_CONTENT_TYPE'] = $type;
}
if (isset($params)) {
if (is_array($params)) {
$params = implode("&", array_map(function($v, $k) {
return rawurlencode($k).(isset($v) ? "=".rawurlencode($v) : "");
}, $params, array_keys($params)));
}
$url = URL::queryAppend($url, (string) $params);
}
$q = parse_url($url, \PHP_URL_QUERY);
if (strlen($q ?? "")) {
parse_str($q, $params);
} else {
$params = [];
}
$parsedBody = null;
if (isset($body)) {
if (is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) {
parse_str($body, $parsedBody);
} elseif (!is_string($body) && in_array(strtolower($type), ["application/json", "text/json"])) {
$body = json_encode($body, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
} elseif (!is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) {
$parsedBody = $body;
$body = http_build_query($body, "a", "&");
}
}
$server = array_merge($server, $vars);
$req = new ServerRequest($server, [], $url, $method, "php://memory", [], [], $params, $parsedBody);
if (isset($user)) {
if (strlen($user)) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
} else {
$req = $req->withAttribute("authenticationFailed", true);
}
}
if (strlen($type) &&strlen($body ?? "")) {
$req = $req->withHeader("Content-Type", $type);
}
foreach ($headers as $key => $value) {
if (!is_null($value)) {
$req = $req->withHeader($key, $value);
} else {
$req = $req->withoutHeader($key);
}
}
$target = substr(URL::normalize($url), strlen($urlPrefix));
$req = $req->withRequestTarget($target);
if (strlen($body ?? "")) {
$p = $req->getBody();
$p->write($body);
$req = $req->withBody($p);
}
return $req;
}
public function assertException($msg = "", string $prefix = "", string $type = "Exception") {
if (func_num_args()) {
if ($msg instanceof \JKingWeb\Arsse\AbstractException) {

View file

@ -98,6 +98,7 @@
<file>cases/Db/MySQLPDO/TestUpdate.php</file>
</testsuite>
<testsuite name="Database functions">
<file>cases/Database/TestDatabase.php</file>
<file>cases/Db/SQLite3/TestDatabase.php</file>
<file>cases/Db/SQLite3PDO/TestDatabase.php</file>
<file>cases/Db/PostgreSQL/TestDatabase.php</file>