diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 08b69799..c020b8a3 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -333,6 +333,33 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } + protected function editUser(string $user, array $data): array { + // map Miniflux properties to internal metadata properties + $in = []; + foreach (self::USER_META_MAP as $i => [$o,,]) { + if (isset($data[$i])) { + if ($i === "entry_sorting_direction") { + $in[$o] = $data[$i] === "asc"; + } else { + $in[$o] = $data[$i]; + } + } + } + // make any requested changes + $tr = Arsse::$user->begin(); + if ($in) { + Arsse::$user->propertiesSet($user, $in); + } + // read out the newly-modified user and commit the changes + $out = $this->listUsers([$user], true)[0]; + $tr->commit(); + // add the input password if a password change was requested + if (isset($data['password'])) { + $out['password'] = $data['password']; + } + return $out; + } + protected function discoverSubscriptions(array $data): ResponseInterface { try { $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); @@ -378,6 +405,33 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } + protected function createUser(array $data): ResponseInterface { + if ($data['username'] === null) { + return new ErrorResponse(["MissingInputValue", 'field' => "username"], 422); + } elseif ($data['password'] === null) { + return new ErrorResponse(["MissingInputValue", 'field' => "password"], 422); + } + try { + $tr = Arsse::$user->begin(); + $data['password'] = Arsse::$user->add($data['username'], $data['password']); + $out = $this->editUser($data['username'], $data); + $tr->commit(); + } catch (UserException $e) { + switch ($e->getCode()) { + case 10403: + return new ErrorResponse(["DuplicateUser", 'user' => $data['username']], 409); + case 10441: + return new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422); + case 10443: + return new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422); + case 10444: + return new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422); + } + throw $e; // @codeCoverageIgnore + } + return new Response($out, 201); + } + protected function updateUserByNum(array $path, array $data): ResponseInterface { // this function is restricted to admins unless the affected user and calling user are the same $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); @@ -396,17 +450,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - // map Miniflux properties to internal metadata properties - $in = []; - foreach (self::USER_META_MAP as $i => [$o,,]) { - if (isset($data[$i])) { - if ($i === "entry_sorting_direction") { - $in[$o] = $data[$i] === "asc"; - } else { - $in[$o] = $data[$i]; - } - } - } // make any requested changes try { $tr = Arsse::$user->begin(); @@ -417,11 +460,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (isset($data['password'])) { Arsse::$user->passwordSet($user, $data['password']); } - if ($in) { - Arsse::$user->propertiesSet($user, $in); - } - // read out the newly-modified user and commit the changes - $out = $this->listUsers([$user], true)[0]; + $out = $this->editUser($user, $data); $tr->commit(); } catch (UserException $e) { switch ($e->getCode()) { @@ -436,10 +475,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } throw $e; // @codeCoverageIgnore } - // add the input password if a password change was requested - if (isset($data['password'])) { - $out['password'] = $data['password']; - } return new Response($out); } diff --git a/locale/en.php b/locale/en.php index 6944023c..d67547aa 100644 --- a/locale/en.php +++ b/locale/en.php @@ -11,6 +11,7 @@ return [ 'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.403' => 'Access Forbidden', 'API.Miniflux.Error.404' => 'Resource Not Found', + 'API.Miniflux.Error.MissingInputValue' => 'Required key "{field}" was not present in input', 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 8495d62d..ce962727 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -321,6 +321,61 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ]; } + /** @dataProvider provideUserAdditions */ + public function testAddAUser(array $body, $in1, $out1, $in2, $out2, ResponseInterface $exp): void { + $this->h = $this->createPartialMock(V1::class, ["now"]); + $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("begin")->willReturn($this->transaction); + Arsse::$user->method("propertiesGet")->willReturnCallback(function(string $u, bool $includeLarge) { + if ($u === "john.doe@example.com") { + return ['num' => 1, 'admin' => true]; + } else { + return ['num' => 2, 'admin' => false]; + } + }); + if ($out1 instanceof \Exception) { + Arsse::$user->method("add")->willThrowException($out1); + } else { + Arsse::$user->method("add")->willReturn($in1[1] ?? ""); + } + if ($out2 instanceof \Exception) { + Arsse::$user->method("propertiesSet")->willThrowException($out2); + } else { + Arsse::$user->method("propertiesSet")->willReturn($out2 ?? []); + } + if ($in1 === null) { + Arsse::$user->expects($this->exactly(0))->method("add"); + } else { + Arsse::$user->expects($this->exactly(1))->method("add")->with(...($in1 ?? [])); + } + if ($in2 === null) { + Arsse::$user->expects($this->exactly(0))->method("propertiesSet"); + } else { + Arsse::$user->expects($this->exactly(1))->method("propertiesSet")->with($body['username'], $in2); + } + $this->assertMessage($exp, $this->req("POST", "/users", $body)); + } + + public function provideUserAdditions(): iterable { + $resp1 = array_merge($this->users[1], ['username' => "ook", 'password' => "eek"]); + return [ + [[], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "username"], 422)], + [['username' => "ook"], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "password"], 422)], + [['username' => "ook", 'password' => "eek"], ["ook", "eek"], new ExceptionConflict("alreadyExists"), null, null, new ErrorResponse(["DuplicateUser", 'user' => "ook"], 409)], + [['username' => "j:k", 'password' => "eek"], ["j:k", "eek"], new UserExceptionInput("invalidUsername"), null, null, new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422)], + [['username' => "ook", 'password' => "eek", 'timezone' => "ook"], ["ook", "eek"], "eek", ['tz' => "ook"], new UserExceptionInput("invalidTimezone"), new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422)], + [['username' => "ook", 'password' => "eek", 'entries_per_page' => -1], ["ook", "eek"], "eek", ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422)], + [['username' => "ook", 'password' => "eek", 'theme' => "default"], ["ook", "eek"], "eek", ['theme' => "default"], ['theme' => "default"], new Response($resp1, 201)], + ]; + } + + public function testAddAUserWithoutAuthority(): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['num' => 1, 'admin' => false]); + $this->assertMessage(new ErrorResponse("403", 403), $this->req("POST", "/users", [])); + } + public function testListCategories(): void { \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 1, 'name' => "Science"],