diff --git a/.htaccess b/.htaccess index 7ba4744..b238854 100644 --- a/.htaccess +++ b/.htaccess @@ -14,3 +14,10 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [QSA,L] + + + Require all granted + + + Require all denied + diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index f154bb5..fc5ecaf 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -134,4 +134,20 @@ class ApiUtils return $oldLink; } + + /** + * Format a Tag for the REST API. + * + * @param string $tag Tag name + * @param int $occurrences Number of links using this tag + * + * @return array Link data formatted for the REST API. + */ + public static function formatTag($tag, $occurences) + { + return [ + 'name' => $tag, + 'occurrences' => $occurences, + ]; + } } diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 3a9c035..ffcfd4c 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -68,16 +68,16 @@ class Links extends ApiController } // 'environment' is set by Slim and encapsulate $_SERVER. - $index = index_url($this->ci['environment']); + $indexUrl = index_url($this->ci['environment']); $out = []; - $cpt = 0; + $index = 0; foreach ($links as $link) { if (count($out) >= $limit) { break; } - if ($cpt++ >= $offset) { - $out[] = ApiUtils::formatLink($link, $index); + if ($index++ >= $offset) { + $out[] = ApiUtils::formatLink($link, $indexUrl); } } diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php new file mode 100644 index 0000000..6dd7875 --- /dev/null +++ b/application/api/controllers/Tags.php @@ -0,0 +1,161 @@ +getParam('visibility'); + $tags = $this->linkDb->linksCountPerTag([], $visibility); + + // Return tags from the {offset}th tag, starting from 0. + $offset = $request->getParam('offset'); + if (! empty($offset) && ! ctype_digit($offset)) { + throw new ApiBadParametersException('Invalid offset'); + } + $offset = ! empty($offset) ? intval($offset) : 0; + if ($offset > count($tags)) { + return $response->withJson([], 200, $this->jsonStyle); + } + + // limit parameter is either a number of links or 'all' for everything. + $limit = $request->getParam('limit'); + if (empty($limit)) { + $limit = self::$DEFAULT_LIMIT; + } + if (ctype_digit($limit)) { + $limit = intval($limit); + } elseif ($limit === 'all') { + $limit = count($tags); + } else { + throw new ApiBadParametersException('Invalid limit'); + } + + $out = []; + $index = 0; + foreach ($tags as $tag => $occurrences) { + if (count($out) >= $limit) { + break; + } + if ($index++ >= $offset) { + $out[] = ApiUtils::formatTag($tag, $occurrences); + } + } + + return $response->withJson($out, 200, $this->jsonStyle); + } + + /** + * Return a single formatted tag by its name. + * + * @param Request $request Slim request. + * @param Response $response Slim response. + * @param array $args Path parameters. including the tag name. + * + * @return Response containing the link array. + * + * @throws ApiTagNotFoundException generating a 404 error. + */ + public function getTag($request, $response, $args) + { + $tags = $this->linkDb->linksCountPerTag(); + if (!isset($tags[$args['tagName']])) { + throw new ApiTagNotFoundException(); + } + $out = ApiUtils::formatTag($args['tagName'], $tags[$args['tagName']]); + + return $response->withJson($out, 200, $this->jsonStyle); + } + + /** + * Rename a tag from the given name. + * If the new name provided matches an existing tag, they will be merged. + * + * @param Request $request Slim request. + * @param Response $response Slim response. + * @param array $args Path parameters. including the tag name. + * + * @return Response response. + * + * @throws ApiTagNotFoundException generating a 404 error. + * @throws ApiBadParametersException new tag name not provided + */ + public function putTag($request, $response, $args) + { + $tags = $this->linkDb->linksCountPerTag(); + if (! isset($tags[$args['tagName']])) { + throw new ApiTagNotFoundException(); + } + + $data = $request->getParsedBody(); + if (empty($data['name'])) { + throw new ApiBadParametersException('New tag name is required in the request body'); + } + + $updated = $this->linkDb->renameTag($args['tagName'], $data['name']); + $this->linkDb->save($this->conf->get('resource.page_cache')); + foreach ($updated as $link) { + $this->history->updateLink($link); + } + + $tags = $this->linkDb->linksCountPerTag(); + $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]); + return $response->withJson($out, 200, $this->jsonStyle); + } + + /** + * Delete an existing tag by its name. + * + * @param Request $request Slim request. + * @param Response $response Slim response. + * @param array $args Path parameters. including the tag name. + * + * @return Response response. + * + * @throws ApiTagNotFoundException generating a 404 error. + */ + public function deleteTag($request, $response, $args) + { + $tags = $this->linkDb->linksCountPerTag(); + if (! isset($tags[$args['tagName']])) { + throw new ApiTagNotFoundException(); + } + $updated = $this->linkDb->renameTag($args['tagName'], null); + $this->linkDb->save($this->conf->get('resource.page_cache')); + foreach ($updated as $link) { + $this->history->updateLink($link); + } + + return $response->withStatus(204); + } +} diff --git a/application/api/exceptions/ApiTagNotFoundException.php b/application/api/exceptions/ApiTagNotFoundException.php new file mode 100644 index 0000000..eed5afa --- /dev/null +++ b/application/api/exceptions/ApiTagNotFoundException.php @@ -0,0 +1,32 @@ +message = 'Tag not found'; + } + + /** + * {@inheritdoc} + */ + public function getApiResponse() + { + return $this->buildApiResponse(404); + } +} diff --git a/index.php b/index.php index ddd5dbf..29d67f6 100644 --- a/index.php +++ b/index.php @@ -2176,6 +2176,12 @@ $app->group('/api/v1', function() { $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); + + $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags'); + $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag'); + $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag'); + $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag'); + $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); })->add('\Shaarli\Api\ApiMiddleware'); diff --git a/tests/api/controllers/HistoryTest.php b/tests/api/controllers/history/HistoryTest.php similarity index 100% rename from tests/api/controllers/HistoryTest.php rename to tests/api/controllers/history/HistoryTest.php diff --git a/tests/api/controllers/InfoTest.php b/tests/api/controllers/info/InfoTest.php similarity index 100% rename from tests/api/controllers/InfoTest.php rename to tests/api/controllers/info/InfoTest.php diff --git a/tests/api/controllers/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php similarity index 100% rename from tests/api/controllers/DeleteLinkTest.php rename to tests/api/controllers/links/DeleteLinkTest.php diff --git a/tests/api/controllers/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php similarity index 100% rename from tests/api/controllers/GetLinkIdTest.php rename to tests/api/controllers/links/GetLinkIdTest.php diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php similarity index 100% rename from tests/api/controllers/GetLinksTest.php rename to tests/api/controllers/links/GetLinksTest.php diff --git a/tests/api/controllers/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php similarity index 100% rename from tests/api/controllers/PostLinkTest.php rename to tests/api/controllers/links/PostLinkTest.php diff --git a/tests/api/controllers/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php similarity index 100% rename from tests/api/controllers/PutLinkTest.php rename to tests/api/controllers/links/PutLinkTest.php diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php new file mode 100644 index 0000000..e0787ce --- /dev/null +++ b/tests/api/controllers/tags/DeleteTagTest.php @@ -0,0 +1,164 @@ +conf = new ConfigManager('tests/utils/config/configJson'); + $this->refDB = new \ReferenceLinkDB(); + $this->refDB->write(self::$testDatastore); + $this->linkDB = new \LinkDB(self::$testDatastore, true, false); + $refHistory = new \ReferenceHistory(); + $refHistory->write(self::$testHistory); + $this->history = new \History(self::$testHistory); + $this->container = new Container(); + $this->container['conf'] = $this->conf; + $this->container['db'] = $this->linkDB; + $this->container['history'] = $this->history; + + $this->controller = new Tags($this->container); + } + + /** + * After each test, remove the test datastore. + */ + public function tearDown() + { + @unlink(self::$testDatastore); + @unlink(self::$testHistory); + } + + /** + * Test DELETE tag endpoint: the tag should be removed. + */ + public function testDeleteTagValid() + { + $tagName = 'gnu'; + $tags = $this->linkDB->linksCountPerTag(); + $this->assertTrue($tags[$tagName] > 0); + $env = Environment::mock([ + 'REQUEST_METHOD' => 'DELETE', + ]); + $request = Request::createFromEnvironment($env); + + $response = $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]); + $this->assertEquals(204, $response->getStatusCode()); + $this->assertEmpty((string) $response->getBody()); + + $this->linkDB = new \LinkDB(self::$testDatastore, true, false); + $tags = $this->linkDB->linksCountPerTag(); + $this->assertFalse(isset($tags[$tagName])); + + // 2 links affected + $historyEntry = $this->history->getHistory()[0]; + $this->assertEquals(\History::UPDATED, $historyEntry['event']); + $this->assertTrue( + (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] + ); + $historyEntry = $this->history->getHistory()[1]; + $this->assertEquals(\History::UPDATED, $historyEntry['event']); + $this->assertTrue( + (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] + ); + } + + /** + * Test DELETE tag endpoint: the tag should be removed. + */ + public function testDeleteTagCaseSensitivity() + { + $tagName = 'sTuff'; + $tags = $this->linkDB->linksCountPerTag(); + $this->assertTrue($tags[$tagName] > 0); + $env = Environment::mock([ + 'REQUEST_METHOD' => 'DELETE', + ]); + $request = Request::createFromEnvironment($env); + + $response = $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]); + $this->assertEquals(204, $response->getStatusCode()); + $this->assertEmpty((string) $response->getBody()); + + $this->linkDB = new \LinkDB(self::$testDatastore, true, false); + $tags = $this->linkDB->linksCountPerTag(); + $this->assertFalse(isset($tags[$tagName])); + $this->assertTrue($tags[strtolower($tagName)] > 0); + + $historyEntry = $this->history->getHistory()[0]; + $this->assertEquals(\History::UPDATED, $historyEntry['event']); + $this->assertTrue( + (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] + ); + } + + /** + * Test DELETE tag endpoint: reach not existing tag. + * + * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException + * @expectedExceptionMessage Tag not found + */ + public function testDeleteLink404() + { + $tagName = 'nopenope'; + $tags = $this->linkDB->linksCountPerTag(); + $this->assertFalse(isset($tags[$tagName])); + $env = Environment::mock([ + 'REQUEST_METHOD' => 'DELETE', + ]); + $request = Request::createFromEnvironment($env); + + $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]); + } +} diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php new file mode 100644 index 0000000..afac228 --- /dev/null +++ b/tests/api/controllers/tags/GetTagNameTest.php @@ -0,0 +1,129 @@ +conf = new ConfigManager('tests/utils/config/configJson'); + $this->refDB = new \ReferenceLinkDB(); + $this->refDB->write(self::$testDatastore); + + $this->container = new Container(); + $this->container['conf'] = $this->conf; + $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); + $this->container['history'] = null; + + $this->controller = new Tags($this->container); + } + + /** + * After each test, remove the test datastore. + */ + public function tearDown() + { + @unlink(self::$testDatastore); + } + + /** + * Test basic getTag service: return gnu tag with 2 occurrences. + */ + public function testGetTag() + { + $tagName = 'gnu'; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + ]); + $request = Request::createFromEnvironment($env); + + $response = $this->controller->getTag($request, new Response(), ['tagName' => $tagName]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_TAG, count($data)); + $this->assertEquals($tagName, $data['name']); + $this->assertEquals(2, $data['occurrences']); + } + + /** + * Test getTag service which is not case sensitive: occurrences with both sTuff and stuff + */ + public function testGetTagNotCaseSensitive() + { + $tagName = 'sTuff'; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + ]); + $request = Request::createFromEnvironment($env); + + $response = $this->controller->getTag($request, new Response(), ['tagName' => $tagName]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_TAG, count($data)); + $this->assertEquals($tagName, $data['name']); + $this->assertEquals(2, $data['occurrences']); + } + + /** + * Test basic getTag service: get non existent tag => ApiTagNotFoundException. + * + * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException + * @expectedExceptionMessage Tag not found + */ + public function testGetTag404() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + ]); + $request = Request::createFromEnvironment($env); + + $this->controller->getTag($request, new Response(), ['tagName' => 'nopenope']); + } +} diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php new file mode 100644 index 0000000..3fab31b --- /dev/null +++ b/tests/api/controllers/tags/GetTagsTest.php @@ -0,0 +1,209 @@ +conf = new ConfigManager('tests/utils/config/configJson'); + $this->refDB = new \ReferenceLinkDB(); + $this->refDB->write(self::$testDatastore); + + $this->container = new Container(); + $this->container['conf'] = $this->conf; + $this->linkDB = new \LinkDB(self::$testDatastore, true, false); + $this->container['db'] = $this->linkDB; + $this->container['history'] = null; + + $this->controller = new Tags($this->container); + } + + /** + * After every test, remove the test datastore. + */ + public function tearDown() + { + @unlink(self::$testDatastore); + } + + /** + * Test basic getTags service: returns all tags. + */ + public function testGetTagsAll() + { + $tags = $this->linkDB->linksCountPerTag(); + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + ]); + $request = Request::createFromEnvironment($env); + + $response = $this->controller->getTags($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(count($tags), count($data)); + + // Check order + $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); + $this->assertEquals('web', $data[0]['name']); + $this->assertEquals(4, $data[0]['occurrences']); + $this->assertEquals(self::NB_FIELDS_TAG, count($data[1])); + $this->assertEquals('cartoon', $data[1]['name']); + $this->assertEquals(3, $data[1]['occurrences']); + // Case insensitive + $this->assertEquals(self::NB_FIELDS_TAG, count($data[5])); + $this->assertEquals('sTuff', $data[5]['name']); + $this->assertEquals(2, $data[5]['occurrences']); + // End + $this->assertEquals(self::NB_FIELDS_TAG, count($data[count($data) - 1])); + $this->assertEquals('w3c', $data[count($data) - 1]['name']); + $this->assertEquals(1, $data[count($data) - 1]['occurrences']); + } + + /** + * Test getTags service with offset and limit parameter: + * limit=1 and offset=1 should return only the second tag, cartoon with 3 occurrences + */ + public function testGetTagsOffsetLimit() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => 'offset=1&limit=1' + ]); + $request = Request::createFromEnvironment($env); + $response = $this->controller->getTags($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(1, count($data)); + $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); + $this->assertEquals('cartoon', $data[0]['name']); + $this->assertEquals(3, $data[0]['occurrences']); + } + + /** + * Test getTags with limit=all (return all tags). + */ + public function testGetTagsLimitAll() + { + $tags = $this->linkDB->linksCountPerTag(); + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => 'limit=all' + ]); + $request = Request::createFromEnvironment($env); + $response = $this->controller->getTags($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(count($tags), count($data)); + } + + /** + * Test getTags service with offset and limit parameter: + * limit=1 and offset=1 should not return any tag + */ + public function testGetTagsOffsetTooHigh() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => 'offset=100' + ]); + $request = Request::createFromEnvironment($env); + $response = $this->controller->getTags($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEmpty(count($data)); + } + + /** + * Test getTags with visibility parameter set to private + */ + public function testGetTagsVisibilityPrivate() + { + $tags = $this->linkDB->linksCountPerTag([], 'private'); + $env = Environment::mock([ + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => 'visibility=private' + ]); + $request = Request::createFromEnvironment($env); + $response = $this->controller->getTags($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(count($tags), count($data)); + $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); + $this->assertEquals('Mercurial', $data[0]['name']); + $this->assertEquals(1, $data[0]['occurrences']); + } + + /** + * Test getTags with visibility parameter set to public + */ + public function testGetTagsVisibilityPublic() + { + $tags = $this->linkDB->linksCountPerTag([], 'public'); + $env = Environment::mock( + [ + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => 'visibility=public' + ] + ); + $request = Request::createFromEnvironment($env); + $response = $this->controller->getTags($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string)$response->getBody(), true); + $this->assertEquals(count($tags), count($data)); + $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); + $this->assertEquals('web', $data[0]['name']); + $this->assertEquals(3, $data[0]['occurrences']); + } +} diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php new file mode 100644 index 0000000..6f7dec2 --- /dev/null +++ b/tests/api/controllers/tags/PutTagTest.php @@ -0,0 +1,209 @@ +conf = new ConfigManager('tests/utils/config/configJson.json.php'); + $this->refDB = new \ReferenceLinkDB(); + $this->refDB->write(self::$testDatastore); + + $refHistory = new \ReferenceHistory(); + $refHistory->write(self::$testHistory); + $this->history = new \History(self::$testHistory); + + $this->container = new Container(); + $this->container['conf'] = $this->conf; + $this->linkDB = new \LinkDB(self::$testDatastore, true, false); + $this->container['db'] = $this->linkDB; + $this->container['history'] = $this->history; + + $this->controller = new Tags($this->container); + } + + /** + * After every test, remove the test datastore. + */ + public function tearDown() + { + @unlink(self::$testDatastore); + @unlink(self::$testHistory); + } + + /** + * Test tags update + */ + public function testPutLinkValid() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $tagName = 'gnu'; + $update = ['name' => $newName = 'newtag']; + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($update); + + $response = $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_TAG, count($data)); + $this->assertEquals($newName, $data['name']); + $this->assertEquals(2, $data['occurrences']); + + $tags = $this->linkDB->linksCountPerTag(); + $this->assertNotTrue(isset($tags[$tagName])); + $this->assertEquals(2, $tags[$newName]); + + $historyEntry = $this->history->getHistory()[0]; + $this->assertEquals(\History::UPDATED, $historyEntry['event']); + $this->assertTrue( + (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] + ); + $historyEntry = $this->history->getHistory()[1]; + $this->assertEquals(\History::UPDATED, $historyEntry['event']); + $this->assertTrue( + (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] + ); + } + + /** + * Test tag update with an existing tag: they should be merged + */ + public function testPutTagMerge() + { + $tagName = 'gnu'; + $newName = 'w3c'; + + $tags = $this->linkDB->linksCountPerTag(); + $this->assertEquals(1, $tags[$newName]); + $this->assertEquals(2, $tags[$tagName]); + + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $update = ['name' => $newName]; + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($update); + + $response = $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_TAG, count($data)); + $this->assertEquals($newName, $data['name']); + $this->assertEquals(3, $data['occurrences']); + + $tags = $this->linkDB->linksCountPerTag(); + $this->assertNotTrue(isset($tags[$tagName])); + $this->assertEquals(3, $tags[$newName]); + } + + /** + * Test tag update with an empty new tag name => ApiBadParametersException + * + * @expectedException Shaarli\Api\Exceptions\ApiBadParametersException + * @expectedExceptionMessage New tag name is required in the request body + */ + public function testPutTagEmpty() + { + $tagName = 'gnu'; + $newName = ''; + + $tags = $this->linkDB->linksCountPerTag(); + $this->assertEquals(2, $tags[$tagName]); + + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $request = Request::createFromEnvironment($env); + + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $update = ['name' => $newName]; + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($update); + + try { + $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); + } catch (ApiBadParametersException $e) { + $tags = $this->linkDB->linksCountPerTag(); + $this->assertEquals(2, $tags[$tagName]); + throw $e; + } + } + + /** + * Test tag update on non existent tag => ApiTagNotFoundException. + * + * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException + * @expectedExceptionMessage Tag not found + */ + public function testPutTag404() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $request = Request::createFromEnvironment($env); + + $this->controller->putTag($request, new Response(), ['tagName' => 'nopenope']); + } +}