1
1
Fork 0
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:
J. King 2020-11-30 10:52:32 -05:00
parent 8c059773bb
commit def07bb1ad
3 changed files with 60 additions and 14 deletions

View file

@ -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);
} }

View file

@ -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,

View file

@ -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>