mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Tests for Miniflux authentication
This appears to match Miniflux's behaviour
This commit is contained in:
parent
8c059773bb
commit
def07bb1ad
3 changed files with 60 additions and 14 deletions
|
@ -7,12 +7,14 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST\Miniflux;
|
namespace JKingWeb\Arsse\REST\Miniflux;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
|
||||||
use JKingWeb\Arsse\AbstractException;
|
use JKingWeb\Arsse\AbstractException;
|
||||||
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
use JKingWeb\Arsse\Misc\HTTP;
|
use JKingWeb\Arsse\Misc\HTTP;
|
||||||
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||||
use JKingWeb\Arsse\REST\Exception;
|
use JKingWeb\Arsse\REST\Exception;
|
||||||
use JKingWeb\Arsse\REST\Exception404;
|
use JKingWeb\Arsse\REST\Exception404;
|
||||||
use JKingWeb\Arsse\REST\Exception405;
|
use JKingWeb\Arsse\REST\Exception405;
|
||||||
|
use JKingWeb\Arsse\REST\Exception501;
|
||||||
use JKingWeb\Arsse\User\ExceptionConflict as UserException;
|
use JKingWeb\Arsse\User\ExceptionConflict as UserException;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
@ -21,6 +23,7 @@ use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"];
|
protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"];
|
||||||
protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"];
|
protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"];
|
||||||
|
protected const TOKEN_LENGTH = 32;
|
||||||
public const VERSION = "2.0.25";
|
public const VERSION = "2.0.25";
|
||||||
|
|
||||||
protected $paths = [
|
protected $paths = [
|
||||||
|
@ -50,21 +53,22 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
|
||||||
protected function authenticate(ServerRequestInterface $req): bool {
|
protected function authenticate(ServerRequestInterface $req): bool {
|
||||||
// first check any tokens; this is what Miniflux does
|
// first check any tokens; this is what Miniflux does
|
||||||
foreach ($req->getHeader("X-Auth-Token") as $t) {
|
if ($req->hasHeader("X-Auth-Token")) {
|
||||||
if (strlen($t)) {
|
$t = $req->getHeader("X-Auth-Token")[0]; // consider only the first token
|
||||||
// a non-empty header is authoritative, so we'll stop here one way or the other
|
if (strlen($t)) { // and only if it is not blank
|
||||||
try {
|
try {
|
||||||
$d = Arsse::$db->tokenLookup("miniflux.login", $t);
|
$d = Arsse::$db->tokenLookup("miniflux.login", $t);
|
||||||
} catch (ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Arsse::$user->id = $d->user;
|
Arsse::$user->id = $d['user'];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// next check HTTP auth
|
// next check HTTP auth
|
||||||
if ($req->getAttribute("authenticated", false)) {
|
if ($req->getAttribute("authenticated", false)) {
|
||||||
Arsse::$user->id = $req->getAttribute("authenticatedUser");
|
Arsse::$user->id = $req->getAttribute("authenticatedUser");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -84,11 +88,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$func = $this->chooseCall($target, $method);
|
$func = $this->chooseCall($target, $method);
|
||||||
if ($func === "opmlImport") {
|
if ($func === "opmlImport") {
|
||||||
if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) {
|
if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) {
|
||||||
return new ErrorResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]);
|
return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]);
|
||||||
}
|
}
|
||||||
$data = (string) $req->getBody();
|
$data = (string) $req->getBody();
|
||||||
} elseif ($method === "POST" || $method === "PUT") {
|
} elseif ($method === "POST" || $method === "PUT") {
|
||||||
$data = @json_decode($data, true);
|
$data = @json_decode((string) $req->getBody(), true);
|
||||||
if (json_last_error() !== \JSON_ERROR_NONE) {
|
if (json_last_error() !== \JSON_ERROR_NONE) {
|
||||||
// if the body could not be parsed as JSON, return "400 Bad Request"
|
// if the body could not be parsed as JSON, return "400 Bad Request"
|
||||||
return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400);
|
return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400);
|
||||||
|
@ -172,7 +176,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function tokenGenerate(string $user, string $label): string {
|
public static function tokenGenerate(string $user, string $label): string {
|
||||||
$t = base64_encode(random_bytes(24));
|
// Miniflux produces tokens in base64url alphabet
|
||||||
|
$t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH)));
|
||||||
return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label);
|
return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,19 @@ use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\User;
|
use JKingWeb\Arsse\User;
|
||||||
use JKingWeb\Arsse\Database;
|
use JKingWeb\Arsse\Database;
|
||||||
use JKingWeb\Arsse\Db\Transaction;
|
use JKingWeb\Arsse\Db\Transaction;
|
||||||
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
|
use JKingWeb\Arsse\REST\Exception404;
|
||||||
use JKingWeb\Arsse\REST\Miniflux\V1;
|
use JKingWeb\Arsse\REST\Miniflux\V1;
|
||||||
|
use JKingWeb\Arsse\REST\Miniflux\ErrorResponse;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Laminas\Diactoros\Response\JsonResponse as Response;
|
||||||
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\REST\Miniflux\V1<extended> */
|
/** @covers \JKingWeb\Arsse\REST\Miniflux\V1<extended> */
|
||||||
class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
|
class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
protected $h;
|
protected $h;
|
||||||
protected $transaction;
|
protected $transaction;
|
||||||
|
protected $token = "Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc=";
|
||||||
|
|
||||||
protected function req(string $method, string $target, $data = "", array $headers = [], bool $authenticated = true, bool $body = true): ResponseInterface {
|
protected function req(string $method, string $target, $data = "", array $headers = [], bool $authenticated = true, bool $body = true): ResponseInterface {
|
||||||
$prefix = "/v1";
|
$prefix = "/v1";
|
||||||
|
@ -54,13 +60,47 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider provideAuthResponses */
|
/** @dataProvider provideAuthResponses */
|
||||||
public function testAuthenticateAUser(): void {
|
public function testAuthenticateAUser($token, bool $auth, bool $success): void {
|
||||||
$exp = new EmptyResponse(401);
|
$exp = new ErrorResponse("401", 401);
|
||||||
$this->assertMessage($exp, $this->req("GET", "/", "", [], false));
|
$user = "john.doe@example.com";
|
||||||
|
if ($token !== null) {
|
||||||
|
$headers = ['X-Auth-Token' => $token];
|
||||||
|
} else {
|
||||||
|
$headers = [];
|
||||||
|
}
|
||||||
|
Arsse::$user->id = null;
|
||||||
|
\Phake::when(Arsse::$db)->tokenLookup->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
|
\Phake::when(Arsse::$db)->tokenLookup("miniflux.login", $this->token)->thenReturn(['user' => $user]);
|
||||||
|
if ($success) {
|
||||||
|
$this->expectExceptionObject(new Exception404);
|
||||||
|
try {
|
||||||
|
$this->req("GET", "/", "", $headers, $auth);
|
||||||
|
} finally {
|
||||||
|
$this->assertSame($user, Arsse::$user->id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth));
|
||||||
|
$this->assertNull(Arsse::$user->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideAuthResponses(): iterable {
|
||||||
|
return [
|
||||||
|
[null, false, false],
|
||||||
|
[null, true, true],
|
||||||
|
[$this->token, false, true],
|
||||||
|
[[$this->token, "BOGUS"], false, true],
|
||||||
|
["", true, true],
|
||||||
|
[["", "BOGUS"], true, true],
|
||||||
|
["NOT A TOKEN", false, false],
|
||||||
|
["NOT A TOKEN", true, false],
|
||||||
|
[["BOGUS", $this->token], false, false],
|
||||||
|
[["", $this->token], false, false],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider provideInvalidPaths */
|
/** @dataProvider provideInvalidPaths */
|
||||||
public function testRespondToInvalidPaths($path, $method, $code, $allow = null): void {
|
public function xtestRespondToInvalidPaths($path, $method, $code, $allow = null): void {
|
||||||
$exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []);
|
$exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []);
|
||||||
$this->assertMessage($exp, $this->req($method, $path));
|
$this->assertMessage($exp, $this->req($method, $path));
|
||||||
}
|
}
|
||||||
|
@ -72,7 +112,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRespondToInvalidInputTypes(): void {
|
public function xtestRespondToInvalidInputTypes(): void {
|
||||||
$exp = new EmptyResponse(415, ['Accept' => "application/json"]);
|
$exp = new EmptyResponse(415, ['Accept' => "application/json"]);
|
||||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => "application/xml"]));
|
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => "application/xml"]));
|
||||||
$exp = new EmptyResponse(400);
|
$exp = new EmptyResponse(400);
|
||||||
|
@ -81,7 +121,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider provideOptionsRequests */
|
/** @dataProvider provideOptionsRequests */
|
||||||
public function testRespondToOptionsRequests(string $url, string $allow, string $accept): void {
|
public function xtestRespondToOptionsRequests(string $url, string $allow, string $accept): void {
|
||||||
$exp = new EmptyResponse(204, [
|
$exp = new EmptyResponse(204, [
|
||||||
'Allow' => $allow,
|
'Allow' => $allow,
|
||||||
'Accept' => $accept,
|
'Accept' => $accept,
|
||||||
|
|
|
@ -115,6 +115,7 @@
|
||||||
<testsuite name="Miniflux">
|
<testsuite name="Miniflux">
|
||||||
<file>cases/REST/Miniflux/TestErrorResponse.php</file>
|
<file>cases/REST/Miniflux/TestErrorResponse.php</file>
|
||||||
<file>cases/REST/Miniflux/TestStatus.php</file>
|
<file>cases/REST/Miniflux/TestStatus.php</file>
|
||||||
|
<file>cases/REST/Miniflux/TestV1.php</file>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
<testsuite name="NCNv1">
|
<testsuite name="NCNv1">
|
||||||
<file>cases/REST/NextcloudNews/TestVersions.php</file>
|
<file>cases/REST/NextcloudNews/TestVersions.php</file>
|
||||||
|
|
Loading…
Reference in a new issue