diff options
18 files changed, 942 insertions, 6 deletions
@@ -14,3 +14,10 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] | |||
14 | RewriteCond %{REQUEST_FILENAME} !-f | 14 | RewriteCond %{REQUEST_FILENAME} !-f |
15 | RewriteCond %{REQUEST_FILENAME} !-d | 15 | RewriteCond %{REQUEST_FILENAME} !-d |
16 | RewriteRule ^ index.php [QSA,L] | 16 | RewriteRule ^ index.php [QSA,L] |
17 | |||
18 | <Limit GET POST PUT DELETE OPTIONS> | ||
19 | Require all granted | ||
20 | </Limit> | ||
21 | <LimitExcept GET POST PUT DELETE OPTIONS> | ||
22 | Require all denied | ||
23 | </LimitExcept> | ||
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index f154bb52..fc5ecaf1 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php | |||
@@ -134,4 +134,20 @@ class ApiUtils | |||
134 | 134 | ||
135 | return $oldLink; | 135 | return $oldLink; |
136 | } | 136 | } |
137 | |||
138 | /** | ||
139 | * Format a Tag for the REST API. | ||
140 | * | ||
141 | * @param string $tag Tag name | ||
142 | * @param int $occurrences Number of links using this tag | ||
143 | * | ||
144 | * @return array Link data formatted for the REST API. | ||
145 | */ | ||
146 | public static function formatTag($tag, $occurences) | ||
147 | { | ||
148 | return [ | ||
149 | 'name' => $tag, | ||
150 | 'occurrences' => $occurences, | ||
151 | ]; | ||
152 | } | ||
137 | } | 153 | } |
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 3a9c0355..ffcfd4c7 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php | |||
@@ -68,16 +68,16 @@ class Links extends ApiController | |||
68 | } | 68 | } |
69 | 69 | ||
70 | // 'environment' is set by Slim and encapsulate $_SERVER. | 70 | // 'environment' is set by Slim and encapsulate $_SERVER. |
71 | $index = index_url($this->ci['environment']); | 71 | $indexUrl = index_url($this->ci['environment']); |
72 | 72 | ||
73 | $out = []; | 73 | $out = []; |
74 | $cpt = 0; | 74 | $index = 0; |
75 | foreach ($links as $link) { | 75 | foreach ($links as $link) { |
76 | if (count($out) >= $limit) { | 76 | if (count($out) >= $limit) { |
77 | break; | 77 | break; |
78 | } | 78 | } |
79 | if ($cpt++ >= $offset) { | 79 | if ($index++ >= $offset) { |
80 | $out[] = ApiUtils::formatLink($link, $index); | 80 | $out[] = ApiUtils::formatLink($link, $indexUrl); |
81 | } | 81 | } |
82 | } | 82 | } |
83 | 83 | ||
diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php new file mode 100644 index 00000000..6dd78750 --- /dev/null +++ b/application/api/controllers/Tags.php | |||
@@ -0,0 +1,161 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Api\Controllers; | ||
4 | |||
5 | use Shaarli\Api\ApiUtils; | ||
6 | use Shaarli\Api\Exceptions\ApiBadParametersException; | ||
7 | use Shaarli\Api\Exceptions\ApiLinkNotFoundException; | ||
8 | use Shaarli\Api\Exceptions\ApiTagNotFoundException; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Class Tags | ||
14 | * | ||
15 | * REST API Controller: all services related to tags collection. | ||
16 | * | ||
17 | * @package Api\Controllers | ||
18 | */ | ||
19 | class Tags extends ApiController | ||
20 | { | ||
21 | /** | ||
22 | * @var int Number of links returned if no limit is provided. | ||
23 | */ | ||
24 | public static $DEFAULT_LIMIT = 'all'; | ||
25 | |||
26 | /** | ||
27 | * Retrieve a list of tags, allowing different filters. | ||
28 | * | ||
29 | * @param Request $request Slim request. | ||
30 | * @param Response $response Slim response. | ||
31 | * | ||
32 | * @return Response response. | ||
33 | * | ||
34 | * @throws ApiBadParametersException Invalid parameters. | ||
35 | */ | ||
36 | public function getTags($request, $response) | ||
37 | { | ||
38 | $visibility = $request->getParam('visibility'); | ||
39 | $tags = $this->linkDb->linksCountPerTag([], $visibility); | ||
40 | |||
41 | // Return tags from the {offset}th tag, starting from 0. | ||
42 | $offset = $request->getParam('offset'); | ||
43 | if (! empty($offset) && ! ctype_digit($offset)) { | ||
44 | throw new ApiBadParametersException('Invalid offset'); | ||
45 | } | ||
46 | $offset = ! empty($offset) ? intval($offset) : 0; | ||
47 | if ($offset > count($tags)) { | ||
48 | return $response->withJson([], 200, $this->jsonStyle); | ||
49 | } | ||
50 | |||
51 | // limit parameter is either a number of links or 'all' for everything. | ||
52 | $limit = $request->getParam('limit'); | ||
53 | if (empty($limit)) { | ||
54 | $limit = self::$DEFAULT_LIMIT; | ||
55 | } | ||
56 | if (ctype_digit($limit)) { | ||
57 | $limit = intval($limit); | ||
58 | } elseif ($limit === 'all') { | ||
59 | $limit = count($tags); | ||
60 | } else { | ||
61 | throw new ApiBadParametersException('Invalid limit'); | ||
62 | } | ||
63 | |||
64 | $out = []; | ||
65 | $index = 0; | ||
66 | foreach ($tags as $tag => $occurrences) { | ||
67 | if (count($out) >= $limit) { | ||
68 | break; | ||
69 | } | ||
70 | if ($index++ >= $offset) { | ||
71 | $out[] = ApiUtils::formatTag($tag, $occurrences); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | return $response->withJson($out, 200, $this->jsonStyle); | ||
76 | } | ||
77 | |||
78 | /** | ||
79 | * Return a single formatted tag by its name. | ||
80 | * | ||
81 | * @param Request $request Slim request. | ||
82 | * @param Response $response Slim response. | ||
83 | * @param array $args Path parameters. including the tag name. | ||
84 | * | ||
85 | * @return Response containing the link array. | ||
86 | * | ||
87 | * @throws ApiTagNotFoundException generating a 404 error. | ||
88 | */ | ||
89 | public function getTag($request, $response, $args) | ||
90 | { | ||
91 | $tags = $this->linkDb->linksCountPerTag(); | ||
92 | if (!isset($tags[$args['tagName']])) { | ||
93 | throw new ApiTagNotFoundException(); | ||
94 | } | ||
95 | $out = ApiUtils::formatTag($args['tagName'], $tags[$args['tagName']]); | ||
96 | |||
97 | return $response->withJson($out, 200, $this->jsonStyle); | ||
98 | } | ||
99 | |||
100 | /** | ||
101 | * Rename a tag from the given name. | ||
102 | * If the new name provided matches an existing tag, they will be merged. | ||
103 | * | ||
104 | * @param Request $request Slim request. | ||
105 | * @param Response $response Slim response. | ||
106 | * @param array $args Path parameters. including the tag name. | ||
107 | * | ||
108 | * @return Response response. | ||
109 | * | ||
110 | * @throws ApiTagNotFoundException generating a 404 error. | ||
111 | * @throws ApiBadParametersException new tag name not provided | ||
112 | */ | ||
113 | public function putTag($request, $response, $args) | ||
114 | { | ||
115 | $tags = $this->linkDb->linksCountPerTag(); | ||
116 | if (! isset($tags[$args['tagName']])) { | ||
117 | throw new ApiTagNotFoundException(); | ||
118 | } | ||
119 | |||
120 | $data = $request->getParsedBody(); | ||
121 | if (empty($data['name'])) { | ||
122 | throw new ApiBadParametersException('New tag name is required in the request body'); | ||
123 | } | ||
124 | |||
125 | $updated = $this->linkDb->renameTag($args['tagName'], $data['name']); | ||
126 | $this->linkDb->save($this->conf->get('resource.page_cache')); | ||
127 | foreach ($updated as $link) { | ||
128 | $this->history->updateLink($link); | ||
129 | } | ||
130 | |||
131 | $tags = $this->linkDb->linksCountPerTag(); | ||
132 | $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]); | ||
133 | return $response->withJson($out, 200, $this->jsonStyle); | ||
134 | } | ||
135 | |||
136 | /** | ||
137 | * Delete an existing tag by its name. | ||
138 | * | ||
139 | * @param Request $request Slim request. | ||
140 | * @param Response $response Slim response. | ||
141 | * @param array $args Path parameters. including the tag name. | ||
142 | * | ||
143 | * @return Response response. | ||
144 | * | ||
145 | * @throws ApiTagNotFoundException generating a 404 error. | ||
146 | */ | ||
147 | public function deleteTag($request, $response, $args) | ||
148 | { | ||
149 | $tags = $this->linkDb->linksCountPerTag(); | ||
150 | if (! isset($tags[$args['tagName']])) { | ||
151 | throw new ApiTagNotFoundException(); | ||
152 | } | ||
153 | $updated = $this->linkDb->renameTag($args['tagName'], null); | ||
154 | $this->linkDb->save($this->conf->get('resource.page_cache')); | ||
155 | foreach ($updated as $link) { | ||
156 | $this->history->updateLink($link); | ||
157 | } | ||
158 | |||
159 | return $response->withStatus(204); | ||
160 | } | ||
161 | } | ||
diff --git a/application/api/exceptions/ApiTagNotFoundException.php b/application/api/exceptions/ApiTagNotFoundException.php new file mode 100644 index 00000000..eed5afa5 --- /dev/null +++ b/application/api/exceptions/ApiTagNotFoundException.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Api\Exceptions; | ||
4 | |||
5 | |||
6 | use Slim\Http\Response; | ||
7 | |||
8 | /** | ||
9 | * Class ApiTagNotFoundException | ||
10 | * | ||
11 | * Tag selected by name couldn't be found in the datastore, results in a 404 error. | ||
12 | * | ||
13 | * @package Shaarli\Api\Exceptions | ||
14 | */ | ||
15 | class ApiTagNotFoundException extends ApiException | ||
16 | { | ||
17 | /** | ||
18 | * ApiLinkNotFoundException constructor. | ||
19 | */ | ||
20 | public function __construct() | ||
21 | { | ||
22 | $this->message = 'Tag not found'; | ||
23 | } | ||
24 | |||
25 | /** | ||
26 | * {@inheritdoc} | ||
27 | */ | ||
28 | public function getApiResponse() | ||
29 | { | ||
30 | return $this->buildApiResponse(404); | ||
31 | } | ||
32 | } | ||
diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md index 6066140e..e53c9422 100644 --- a/doc/md/docker/reverse-proxy-configuration.md +++ b/doc/md/docker/reverse-proxy-configuration.md | |||
@@ -13,12 +13,14 @@ This guide assumes that: | |||
13 | - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) | 13 | - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) |
14 | - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers) | 14 | - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers) |
15 | 15 | ||
16 | The following HTTP headers are set by using the `ProxyPass` directive: | 16 | The following HTTP headers are set when the `ProxyPass` directive is set: |
17 | 17 | ||
18 | - `X-Forwarded-For` | 18 | - `X-Forwarded-For` |
19 | - `X-Forwarded-Host` | 19 | - `X-Forwarded-Host` |
20 | - `X-Forwarded-Server` | 20 | - `X-Forwarded-Server` |
21 | 21 | ||
22 | The original `SERVER_NAME` can be sent to the proxied host by setting the [`ProxyPreserveHost`](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#ProxyPreserveHost) directive to `On`. | ||
23 | |||
22 | ```apache | 24 | ```apache |
23 | <VirtualHost *:80> | 25 | <VirtualHost *:80> |
24 | ServerName shaarli.domain.tld | 26 | ServerName shaarli.domain.tld |
@@ -37,7 +39,8 @@ The following HTTP headers are set by using the `ProxyPass` directive: | |||
37 | CustomLog /var/log/apache2/shaarli-access.log combined | 39 | CustomLog /var/log/apache2/shaarli-access.log combined |
38 | 40 | ||
39 | RequestHeader set X-Forwarded-Proto "https" | 41 | RequestHeader set X-Forwarded-Proto "https" |
40 | 42 | ProxyPreserveHost On | |
43 | |||
41 | ProxyPass / http://127.0.0.1:10080/ | 44 | ProxyPass / http://127.0.0.1:10080/ |
42 | ProxyPassReverse / http://127.0.0.1:10080/ | 45 | ProxyPassReverse / http://127.0.0.1:10080/ |
43 | </VirtualHost> | 46 | </VirtualHost> |
@@ -2176,6 +2176,12 @@ $app->group('/api/v1', function() { | |||
2176 | $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); | 2176 | $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); |
2177 | $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); | 2177 | $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); |
2178 | $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); | 2178 | $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); |
2179 | |||
2180 | $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags'); | ||
2181 | $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag'); | ||
2182 | $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag'); | ||
2183 | $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag'); | ||
2184 | |||
2179 | $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); | 2185 | $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); |
2180 | })->add('\Shaarli\Api\ApiMiddleware'); | 2186 | })->add('\Shaarli\Api\ApiMiddleware'); |
2181 | 2187 | ||
diff --git a/tests/api/controllers/HistoryTest.php b/tests/api/controllers/history/HistoryTest.php index 61046d97..61046d97 100644 --- a/tests/api/controllers/HistoryTest.php +++ b/tests/api/controllers/history/HistoryTest.php | |||
diff --git a/tests/api/controllers/InfoTest.php b/tests/api/controllers/info/InfoTest.php index f7e63bfa..f7e63bfa 100644 --- a/tests/api/controllers/InfoTest.php +++ b/tests/api/controllers/info/InfoTest.php | |||
diff --git a/tests/api/controllers/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php index 7d797137..7d797137 100644 --- a/tests/api/controllers/DeleteLinkTest.php +++ b/tests/api/controllers/links/DeleteLinkTest.php | |||
diff --git a/tests/api/controllers/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php index 57528d5a..57528d5a 100644 --- a/tests/api/controllers/GetLinkIdTest.php +++ b/tests/api/controllers/links/GetLinkIdTest.php | |||
diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php index d22ed3bf..d22ed3bf 100644 --- a/tests/api/controllers/GetLinksTest.php +++ b/tests/api/controllers/links/GetLinksTest.php | |||
diff --git a/tests/api/controllers/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php index 100a9170..100a9170 100644 --- a/tests/api/controllers/PostLinkTest.php +++ b/tests/api/controllers/links/PostLinkTest.php | |||
diff --git a/tests/api/controllers/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php index 8a562571..8a562571 100644 --- a/tests/api/controllers/PutLinkTest.php +++ b/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 00000000..e0787ce2 --- /dev/null +++ b/tests/api/controllers/tags/DeleteTagTest.php | |||
@@ -0,0 +1,164 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | namespace Shaarli\Api\Controllers; | ||
5 | |||
6 | use Shaarli\Config\ConfigManager; | ||
7 | use Slim\Container; | ||
8 | use Slim\Http\Environment; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | class DeleteTagTest extends \PHPUnit_Framework_TestCase | ||
13 | { | ||
14 | /** | ||
15 | * @var string datastore to test write operations | ||
16 | */ | ||
17 | protected static $testDatastore = 'sandbox/datastore.php'; | ||
18 | |||
19 | /** | ||
20 | * @var string datastore to test write operations | ||
21 | */ | ||
22 | protected static $testHistory = 'sandbox/history.php'; | ||
23 | |||
24 | /** | ||
25 | * @var ConfigManager instance | ||
26 | */ | ||
27 | protected $conf; | ||
28 | |||
29 | /** | ||
30 | * @var \ReferenceLinkDB instance. | ||
31 | */ | ||
32 | protected $refDB = null; | ||
33 | |||
34 | /** | ||
35 | * @var \LinkDB instance. | ||
36 | */ | ||
37 | protected $linkDB; | ||
38 | |||
39 | /** | ||
40 | * @var \History instance. | ||
41 | */ | ||
42 | protected $history; | ||
43 | |||
44 | /** | ||
45 | * @var Container instance. | ||
46 | */ | ||
47 | protected $container; | ||
48 | |||
49 | /** | ||
50 | * @var Tags controller instance. | ||
51 | */ | ||
52 | protected $controller; | ||
53 | |||
54 | /** | ||
55 | * Before each test, instantiate a new Api with its config, plugins and links. | ||
56 | */ | ||
57 | public function setUp() | ||
58 | { | ||
59 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | ||
60 | $this->refDB = new \ReferenceLinkDB(); | ||
61 | $this->refDB->write(self::$testDatastore); | ||
62 | $this->linkDB = new \LinkDB(self::$testDatastore, true, false); | ||
63 | $refHistory = new \ReferenceHistory(); | ||
64 | $refHistory->write(self::$testHistory); | ||
65 | $this->history = new \History(self::$testHistory); | ||
66 | $this->container = new Container(); | ||
67 | $this->container['conf'] = $this->conf; | ||
68 | $this->container['db'] = $this->linkDB; | ||
69 | $this->container['history'] = $this->history; | ||
70 | |||
71 | $this->controller = new Tags($this->container); | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * After each test, remove the test datastore. | ||
76 | */ | ||
77 | public function tearDown() | ||
78 | { | ||
79 | @unlink(self::$testDatastore); | ||
80 | @unlink(self::$testHistory); | ||
81 | } | ||
82 | |||
83 | /** | ||
84 | * Test DELETE tag endpoint: the tag should be removed. | ||
85 | */ | ||
86 | public function testDeleteTagValid() | ||
87 | { | ||
88 | $tagName = 'gnu'; | ||
89 | $tags = $this->linkDB->linksCountPerTag(); | ||
90 | $this->assertTrue($tags[$tagName] > 0); | ||
91 | $env = Environment::mock([ | ||
92 | 'REQUEST_METHOD' => 'DELETE', | ||
93 | ]); | ||
94 | $request = Request::createFromEnvironment($env); | ||
95 | |||
96 | $response = $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]); | ||
97 | $this->assertEquals(204, $response->getStatusCode()); | ||
98 | $this->assertEmpty((string) $response->getBody()); | ||
99 | |||
100 | $this->linkDB = new \LinkDB(self::$testDatastore, true, false); | ||
101 | $tags = $this->linkDB->linksCountPerTag(); | ||
102 | $this->assertFalse(isset($tags[$tagName])); | ||
103 | |||
104 | // 2 links affected | ||
105 | $historyEntry = $this->history->getHistory()[0]; | ||
106 | $this->assertEquals(\History::UPDATED, $historyEntry['event']); | ||
107 | $this->assertTrue( | ||
108 | (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] | ||
109 | ); | ||
110 | $historyEntry = $this->history->getHistory()[1]; | ||
111 | $this->assertEquals(\History::UPDATED, $historyEntry['event']); | ||
112 | $this->assertTrue( | ||
113 | (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] | ||
114 | ); | ||
115 | } | ||
116 | |||
117 | /** | ||
118 | * Test DELETE tag endpoint: the tag should be removed. | ||
119 | */ | ||
120 | public function testDeleteTagCaseSensitivity() | ||
121 | { | ||
122 | $tagName = 'sTuff'; | ||
123 | $tags = $this->linkDB->linksCountPerTag(); | ||
124 | $this->assertTrue($tags[$tagName] > 0); | ||
125 | $env = Environment::mock([ | ||
126 | 'REQUEST_METHOD' => 'DELETE', | ||
127 | ]); | ||
128 | $request = Request::createFromEnvironment($env); | ||
129 | |||
130 | $response = $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]); | ||
131 | $this->assertEquals(204, $response->getStatusCode()); | ||
132 | $this->assertEmpty((string) $response->getBody()); | ||
133 | |||
134 | $this->linkDB = new \LinkDB(self::$testDatastore, true, false); | ||
135 | $tags = $this->linkDB->linksCountPerTag(); | ||
136 | $this->assertFalse(isset($tags[$tagName])); | ||
137 | $this->assertTrue($tags[strtolower($tagName)] > 0); | ||
138 | |||
139 | $historyEntry = $this->history->getHistory()[0]; | ||
140 | $this->assertEquals(\History::UPDATED, $historyEntry['event']); | ||
141 | $this->assertTrue( | ||
142 | (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] | ||
143 | ); | ||
144 | } | ||
145 | |||
146 | /** | ||
147 | * Test DELETE tag endpoint: reach not existing tag. | ||
148 | * | ||
149 | * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException | ||
150 | * @expectedExceptionMessage Tag not found | ||
151 | */ | ||
152 | public function testDeleteLink404() | ||
153 | { | ||
154 | $tagName = 'nopenope'; | ||
155 | $tags = $this->linkDB->linksCountPerTag(); | ||
156 | $this->assertFalse(isset($tags[$tagName])); | ||
157 | $env = Environment::mock([ | ||
158 | 'REQUEST_METHOD' => 'DELETE', | ||
159 | ]); | ||
160 | $request = Request::createFromEnvironment($env); | ||
161 | |||
162 | $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]); | ||
163 | } | ||
164 | } | ||
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php new file mode 100644 index 00000000..afac228e --- /dev/null +++ b/tests/api/controllers/tags/GetTagNameTest.php | |||
@@ -0,0 +1,129 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Api\Controllers; | ||
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
6 | |||
7 | use Slim\Container; | ||
8 | use Slim\Http\Environment; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Class GetTagNameTest | ||
14 | * | ||
15 | * Test getTag by tag name API service. | ||
16 | * | ||
17 | * @package Shaarli\Api\Controllers | ||
18 | */ | ||
19 | class GetTagNameTest extends \PHPUnit_Framework_TestCase | ||
20 | { | ||
21 | /** | ||
22 | * @var string datastore to test write operations | ||
23 | */ | ||
24 | protected static $testDatastore = 'sandbox/datastore.php'; | ||
25 | |||
26 | /** | ||
27 | * @var ConfigManager instance | ||
28 | */ | ||
29 | protected $conf; | ||
30 | |||
31 | /** | ||
32 | * @var \ReferenceLinkDB instance. | ||
33 | */ | ||
34 | protected $refDB = null; | ||
35 | |||
36 | /** | ||
37 | * @var Container instance. | ||
38 | */ | ||
39 | protected $container; | ||
40 | |||
41 | /** | ||
42 | * @var Tags controller instance. | ||
43 | */ | ||
44 | protected $controller; | ||
45 | |||
46 | /** | ||
47 | * Number of JSON fields per link. | ||
48 | */ | ||
49 | const NB_FIELDS_TAG = 2; | ||
50 | |||
51 | /** | ||
52 | * Before each test, instantiate a new Api with its config, plugins and links. | ||
53 | */ | ||
54 | public function setUp() | ||
55 | { | ||
56 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | ||
57 | $this->refDB = new \ReferenceLinkDB(); | ||
58 | $this->refDB->write(self::$testDatastore); | ||
59 | |||
60 | $this->container = new Container(); | ||
61 | $this->container['conf'] = $this->conf; | ||
62 | $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); | ||
63 | $this->container['history'] = null; | ||
64 | |||
65 | $this->controller = new Tags($this->container); | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * After each test, remove the test datastore. | ||
70 | */ | ||
71 | public function tearDown() | ||
72 | { | ||
73 | @unlink(self::$testDatastore); | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Test basic getTag service: return gnu tag with 2 occurrences. | ||
78 | */ | ||
79 | public function testGetTag() | ||
80 | { | ||
81 | $tagName = 'gnu'; | ||
82 | $env = Environment::mock([ | ||
83 | 'REQUEST_METHOD' => 'GET', | ||
84 | ]); | ||
85 | $request = Request::createFromEnvironment($env); | ||
86 | |||
87 | $response = $this->controller->getTag($request, new Response(), ['tagName' => $tagName]); | ||
88 | $this->assertEquals(200, $response->getStatusCode()); | ||
89 | $data = json_decode((string) $response->getBody(), true); | ||
90 | $this->assertEquals(self::NB_FIELDS_TAG, count($data)); | ||
91 | $this->assertEquals($tagName, $data['name']); | ||
92 | $this->assertEquals(2, $data['occurrences']); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * Test getTag service which is not case sensitive: occurrences with both sTuff and stuff | ||
97 | */ | ||
98 | public function testGetTagNotCaseSensitive() | ||
99 | { | ||
100 | $tagName = 'sTuff'; | ||
101 | $env = Environment::mock([ | ||
102 | 'REQUEST_METHOD' => 'GET', | ||
103 | ]); | ||
104 | $request = Request::createFromEnvironment($env); | ||
105 | |||
106 | $response = $this->controller->getTag($request, new Response(), ['tagName' => $tagName]); | ||
107 | $this->assertEquals(200, $response->getStatusCode()); | ||
108 | $data = json_decode((string) $response->getBody(), true); | ||
109 | $this->assertEquals(self::NB_FIELDS_TAG, count($data)); | ||
110 | $this->assertEquals($tagName, $data['name']); | ||
111 | $this->assertEquals(2, $data['occurrences']); | ||
112 | } | ||
113 | |||
114 | /** | ||
115 | * Test basic getTag service: get non existent tag => ApiTagNotFoundException. | ||
116 | * | ||
117 | * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException | ||
118 | * @expectedExceptionMessage Tag not found | ||
119 | */ | ||
120 | public function testGetTag404() | ||
121 | { | ||
122 | $env = Environment::mock([ | ||
123 | 'REQUEST_METHOD' => 'GET', | ||
124 | ]); | ||
125 | $request = Request::createFromEnvironment($env); | ||
126 | |||
127 | $this->controller->getTag($request, new Response(), ['tagName' => 'nopenope']); | ||
128 | } | ||
129 | } | ||
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php new file mode 100644 index 00000000..3fab31b0 --- /dev/null +++ b/tests/api/controllers/tags/GetTagsTest.php | |||
@@ -0,0 +1,209 @@ | |||
1 | <?php | ||
2 | namespace Shaarli\Api\Controllers; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | |||
6 | use Slim\Container; | ||
7 | use Slim\Http\Environment; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class GetTagsTest | ||
13 | * | ||
14 | * Test get tag list REST API service. | ||
15 | * | ||
16 | * @package Shaarli\Api\Controllers | ||
17 | */ | ||
18 | class GetTagsTest extends \PHPUnit_Framework_TestCase | ||
19 | { | ||
20 | /** | ||
21 | * @var string datastore to test write operations | ||
22 | */ | ||
23 | protected static $testDatastore = 'sandbox/datastore.php'; | ||
24 | |||
25 | /** | ||
26 | * @var ConfigManager instance | ||
27 | */ | ||
28 | protected $conf; | ||
29 | |||
30 | /** | ||
31 | * @var \ReferenceLinkDB instance. | ||
32 | */ | ||
33 | protected $refDB = null; | ||
34 | |||
35 | /** | ||
36 | * @var Container instance. | ||
37 | */ | ||
38 | protected $container; | ||
39 | |||
40 | /** | ||
41 | * @var \LinkDB instance. | ||
42 | */ | ||
43 | protected $linkDB; | ||
44 | |||
45 | /** | ||
46 | * @var Tags controller instance. | ||
47 | */ | ||
48 | protected $controller; | ||
49 | |||
50 | /** | ||
51 | * Number of JSON field per link. | ||
52 | */ | ||
53 | const NB_FIELDS_TAG = 2; | ||
54 | |||
55 | /** | ||
56 | * Before every test, instantiate a new Api with its config, plugins and links. | ||
57 | */ | ||
58 | public function setUp() | ||
59 | { | ||
60 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | ||
61 | $this->refDB = new \ReferenceLinkDB(); | ||
62 | $this->refDB->write(self::$testDatastore); | ||
63 | |||
64 | $this->container = new Container(); | ||
65 | $this->container['conf'] = $this->conf; | ||
66 | $this->linkDB = new \LinkDB(self::$testDatastore, true, false); | ||
67 | $this->container['db'] = $this->linkDB; | ||
68 | $this->container['history'] = null; | ||
69 | |||
70 | $this->controller = new Tags($this->container); | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * After every test, remove the test datastore. | ||
75 | */ | ||
76 | public function tearDown() | ||
77 | { | ||
78 | @unlink(self::$testDatastore); | ||
79 | } | ||
80 | |||
81 | /** | ||
82 | * Test basic getTags service: returns all tags. | ||
83 | */ | ||
84 | public function testGetTagsAll() | ||
85 | { | ||
86 | $tags = $this->linkDB->linksCountPerTag(); | ||
87 | $env = Environment::mock([ | ||
88 | 'REQUEST_METHOD' => 'GET', | ||
89 | ]); | ||
90 | $request = Request::createFromEnvironment($env); | ||
91 | |||
92 | $response = $this->controller->getTags($request, new Response()); | ||
93 | $this->assertEquals(200, $response->getStatusCode()); | ||
94 | $data = json_decode((string) $response->getBody(), true); | ||
95 | $this->assertEquals(count($tags), count($data)); | ||
96 | |||
97 | // Check order | ||
98 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); | ||
99 | $this->assertEquals('web', $data[0]['name']); | ||
100 | $this->assertEquals(4, $data[0]['occurrences']); | ||
101 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[1])); | ||
102 | $this->assertEquals('cartoon', $data[1]['name']); | ||
103 | $this->assertEquals(3, $data[1]['occurrences']); | ||
104 | // Case insensitive | ||
105 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[5])); | ||
106 | $this->assertEquals('sTuff', $data[5]['name']); | ||
107 | $this->assertEquals(2, $data[5]['occurrences']); | ||
108 | // End | ||
109 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[count($data) - 1])); | ||
110 | $this->assertEquals('w3c', $data[count($data) - 1]['name']); | ||
111 | $this->assertEquals(1, $data[count($data) - 1]['occurrences']); | ||
112 | } | ||
113 | |||
114 | /** | ||
115 | * Test getTags service with offset and limit parameter: | ||
116 | * limit=1 and offset=1 should return only the second tag, cartoon with 3 occurrences | ||
117 | */ | ||
118 | public function testGetTagsOffsetLimit() | ||
119 | { | ||
120 | $env = Environment::mock([ | ||
121 | 'REQUEST_METHOD' => 'GET', | ||
122 | 'QUERY_STRING' => 'offset=1&limit=1' | ||
123 | ]); | ||
124 | $request = Request::createFromEnvironment($env); | ||
125 | $response = $this->controller->getTags($request, new Response()); | ||
126 | $this->assertEquals(200, $response->getStatusCode()); | ||
127 | $data = json_decode((string) $response->getBody(), true); | ||
128 | $this->assertEquals(1, count($data)); | ||
129 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); | ||
130 | $this->assertEquals('cartoon', $data[0]['name']); | ||
131 | $this->assertEquals(3, $data[0]['occurrences']); | ||
132 | } | ||
133 | |||
134 | /** | ||
135 | * Test getTags with limit=all (return all tags). | ||
136 | */ | ||
137 | public function testGetTagsLimitAll() | ||
138 | { | ||
139 | $tags = $this->linkDB->linksCountPerTag(); | ||
140 | $env = Environment::mock([ | ||
141 | 'REQUEST_METHOD' => 'GET', | ||
142 | 'QUERY_STRING' => 'limit=all' | ||
143 | ]); | ||
144 | $request = Request::createFromEnvironment($env); | ||
145 | $response = $this->controller->getTags($request, new Response()); | ||
146 | $this->assertEquals(200, $response->getStatusCode()); | ||
147 | $data = json_decode((string) $response->getBody(), true); | ||
148 | $this->assertEquals(count($tags), count($data)); | ||
149 | } | ||
150 | |||
151 | /** | ||
152 | * Test getTags service with offset and limit parameter: | ||
153 | * limit=1 and offset=1 should not return any tag | ||
154 | */ | ||
155 | public function testGetTagsOffsetTooHigh() | ||
156 | { | ||
157 | $env = Environment::mock([ | ||
158 | 'REQUEST_METHOD' => 'GET', | ||
159 | 'QUERY_STRING' => 'offset=100' | ||
160 | ]); | ||
161 | $request = Request::createFromEnvironment($env); | ||
162 | $response = $this->controller->getTags($request, new Response()); | ||
163 | $this->assertEquals(200, $response->getStatusCode()); | ||
164 | $data = json_decode((string) $response->getBody(), true); | ||
165 | $this->assertEmpty(count($data)); | ||
166 | } | ||
167 | |||
168 | /** | ||
169 | * Test getTags with visibility parameter set to private | ||
170 | */ | ||
171 | public function testGetTagsVisibilityPrivate() | ||
172 | { | ||
173 | $tags = $this->linkDB->linksCountPerTag([], 'private'); | ||
174 | $env = Environment::mock([ | ||
175 | 'REQUEST_METHOD' => 'GET', | ||
176 | 'QUERY_STRING' => 'visibility=private' | ||
177 | ]); | ||
178 | $request = Request::createFromEnvironment($env); | ||
179 | $response = $this->controller->getTags($request, new Response()); | ||
180 | $this->assertEquals(200, $response->getStatusCode()); | ||
181 | $data = json_decode((string) $response->getBody(), true); | ||
182 | $this->assertEquals(count($tags), count($data)); | ||
183 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); | ||
184 | $this->assertEquals('Mercurial', $data[0]['name']); | ||
185 | $this->assertEquals(1, $data[0]['occurrences']); | ||
186 | } | ||
187 | |||
188 | /** | ||
189 | * Test getTags with visibility parameter set to public | ||
190 | */ | ||
191 | public function testGetTagsVisibilityPublic() | ||
192 | { | ||
193 | $tags = $this->linkDB->linksCountPerTag([], 'public'); | ||
194 | $env = Environment::mock( | ||
195 | [ | ||
196 | 'REQUEST_METHOD' => 'GET', | ||
197 | 'QUERY_STRING' => 'visibility=public' | ||
198 | ] | ||
199 | ); | ||
200 | $request = Request::createFromEnvironment($env); | ||
201 | $response = $this->controller->getTags($request, new Response()); | ||
202 | $this->assertEquals(200, $response->getStatusCode()); | ||
203 | $data = json_decode((string)$response->getBody(), true); | ||
204 | $this->assertEquals(count($tags), count($data)); | ||
205 | $this->assertEquals(self::NB_FIELDS_TAG, count($data[0])); | ||
206 | $this->assertEquals('web', $data[0]['name']); | ||
207 | $this->assertEquals(3, $data[0]['occurrences']); | ||
208 | } | ||
209 | } | ||
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php new file mode 100644 index 00000000..6f7dec22 --- /dev/null +++ b/tests/api/controllers/tags/PutTagTest.php | |||
@@ -0,0 +1,209 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | namespace Shaarli\Api\Controllers; | ||
5 | |||
6 | |||
7 | use Shaarli\Api\Exceptions\ApiBadParametersException; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Slim\Container; | ||
10 | use Slim\Http\Environment; | ||
11 | use Slim\Http\Request; | ||
12 | use Slim\Http\Response; | ||
13 | |||
14 | class PutTagTest extends \PHPUnit_Framework_TestCase | ||
15 | { | ||
16 | /** | ||
17 | * @var string datastore to test write operations | ||
18 | */ | ||
19 | protected static $testDatastore = 'sandbox/datastore.php'; | ||
20 | |||
21 | /** | ||
22 | * @var string datastore to test write operations | ||
23 | */ | ||
24 | protected static $testHistory = 'sandbox/history.php'; | ||
25 | |||
26 | /** | ||
27 | * @var ConfigManager instance | ||
28 | */ | ||
29 | protected $conf; | ||
30 | |||
31 | /** | ||
32 | * @var \ReferenceLinkDB instance. | ||
33 | */ | ||
34 | protected $refDB = null; | ||
35 | |||
36 | /** | ||
37 | * @var \History instance. | ||
38 | */ | ||
39 | protected $history; | ||
40 | |||
41 | /** | ||
42 | * @var Container instance. | ||
43 | */ | ||
44 | protected $container; | ||
45 | |||
46 | /** | ||
47 | * @var \LinkDB instance. | ||
48 | */ | ||
49 | protected $linkDB; | ||
50 | |||
51 | /** | ||
52 | * @var Tags controller instance. | ||
53 | */ | ||
54 | protected $controller; | ||
55 | |||
56 | /** | ||
57 | * Number of JSON field per link. | ||
58 | */ | ||
59 | const NB_FIELDS_TAG = 2; | ||
60 | |||
61 | /** | ||
62 | * Before every test, instantiate a new Api with its config, plugins and links. | ||
63 | */ | ||
64 | public function setUp() | ||
65 | { | ||
66 | $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); | ||
67 | $this->refDB = new \ReferenceLinkDB(); | ||
68 | $this->refDB->write(self::$testDatastore); | ||
69 | |||
70 | $refHistory = new \ReferenceHistory(); | ||
71 | $refHistory->write(self::$testHistory); | ||
72 | $this->history = new \History(self::$testHistory); | ||
73 | |||
74 | $this->container = new Container(); | ||
75 | $this->container['conf'] = $this->conf; | ||
76 | $this->linkDB = new \LinkDB(self::$testDatastore, true, false); | ||
77 | $this->container['db'] = $this->linkDB; | ||
78 | $this->container['history'] = $this->history; | ||
79 | |||
80 | $this->controller = new Tags($this->container); | ||
81 | } | ||
82 | |||
83 | /** | ||
84 | * After every test, remove the test datastore. | ||
85 | */ | ||
86 | public function tearDown() | ||
87 | { | ||
88 | @unlink(self::$testDatastore); | ||
89 | @unlink(self::$testHistory); | ||
90 | } | ||
91 | |||
92 | /** | ||
93 | * Test tags update | ||
94 | */ | ||
95 | public function testPutLinkValid() | ||
96 | { | ||
97 | $env = Environment::mock([ | ||
98 | 'REQUEST_METHOD' => 'PUT', | ||
99 | ]); | ||
100 | $tagName = 'gnu'; | ||
101 | $update = ['name' => $newName = 'newtag']; | ||
102 | $request = Request::createFromEnvironment($env); | ||
103 | $request = $request->withParsedBody($update); | ||
104 | |||
105 | $response = $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); | ||
106 | $this->assertEquals(200, $response->getStatusCode()); | ||
107 | $data = json_decode((string) $response->getBody(), true); | ||
108 | $this->assertEquals(self::NB_FIELDS_TAG, count($data)); | ||
109 | $this->assertEquals($newName, $data['name']); | ||
110 | $this->assertEquals(2, $data['occurrences']); | ||
111 | |||
112 | $tags = $this->linkDB->linksCountPerTag(); | ||
113 | $this->assertNotTrue(isset($tags[$tagName])); | ||
114 | $this->assertEquals(2, $tags[$newName]); | ||
115 | |||
116 | $historyEntry = $this->history->getHistory()[0]; | ||
117 | $this->assertEquals(\History::UPDATED, $historyEntry['event']); | ||
118 | $this->assertTrue( | ||
119 | (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] | ||
120 | ); | ||
121 | $historyEntry = $this->history->getHistory()[1]; | ||
122 | $this->assertEquals(\History::UPDATED, $historyEntry['event']); | ||
123 | $this->assertTrue( | ||
124 | (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime'] | ||
125 | ); | ||
126 | } | ||
127 | |||
128 | /** | ||
129 | * Test tag update with an existing tag: they should be merged | ||
130 | */ | ||
131 | public function testPutTagMerge() | ||
132 | { | ||
133 | $tagName = 'gnu'; | ||
134 | $newName = 'w3c'; | ||
135 | |||
136 | $tags = $this->linkDB->linksCountPerTag(); | ||
137 | $this->assertEquals(1, $tags[$newName]); | ||
138 | $this->assertEquals(2, $tags[$tagName]); | ||
139 | |||
140 | $env = Environment::mock([ | ||
141 | 'REQUEST_METHOD' => 'PUT', | ||
142 | ]); | ||
143 | $update = ['name' => $newName]; | ||
144 | $request = Request::createFromEnvironment($env); | ||
145 | $request = $request->withParsedBody($update); | ||
146 | |||
147 | $response = $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); | ||
148 | $this->assertEquals(200, $response->getStatusCode()); | ||
149 | $data = json_decode((string) $response->getBody(), true); | ||
150 | $this->assertEquals(self::NB_FIELDS_TAG, count($data)); | ||
151 | $this->assertEquals($newName, $data['name']); | ||
152 | $this->assertEquals(3, $data['occurrences']); | ||
153 | |||
154 | $tags = $this->linkDB->linksCountPerTag(); | ||
155 | $this->assertNotTrue(isset($tags[$tagName])); | ||
156 | $this->assertEquals(3, $tags[$newName]); | ||
157 | } | ||
158 | |||
159 | /** | ||
160 | * Test tag update with an empty new tag name => ApiBadParametersException | ||
161 | * | ||
162 | * @expectedException Shaarli\Api\Exceptions\ApiBadParametersException | ||
163 | * @expectedExceptionMessage New tag name is required in the request body | ||
164 | */ | ||
165 | public function testPutTagEmpty() | ||
166 | { | ||
167 | $tagName = 'gnu'; | ||
168 | $newName = ''; | ||
169 | |||
170 | $tags = $this->linkDB->linksCountPerTag(); | ||
171 | $this->assertEquals(2, $tags[$tagName]); | ||
172 | |||
173 | $env = Environment::mock([ | ||
174 | 'REQUEST_METHOD' => 'PUT', | ||
175 | ]); | ||
176 | $request = Request::createFromEnvironment($env); | ||
177 | |||
178 | $env = Environment::mock([ | ||
179 | 'REQUEST_METHOD' => 'PUT', | ||
180 | ]); | ||
181 | $update = ['name' => $newName]; | ||
182 | $request = Request::createFromEnvironment($env); | ||
183 | $request = $request->withParsedBody($update); | ||
184 | |||
185 | try { | ||
186 | $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); | ||
187 | } catch (ApiBadParametersException $e) { | ||
188 | $tags = $this->linkDB->linksCountPerTag(); | ||
189 | $this->assertEquals(2, $tags[$tagName]); | ||
190 | throw $e; | ||
191 | } | ||
192 | } | ||
193 | |||
194 | /** | ||
195 | * Test tag update on non existent tag => ApiTagNotFoundException. | ||
196 | * | ||
197 | * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException | ||
198 | * @expectedExceptionMessage Tag not found | ||
199 | */ | ||
200 | public function testPutTag404() | ||
201 | { | ||
202 | $env = Environment::mock([ | ||
203 | 'REQUEST_METHOD' => 'PUT', | ||
204 | ]); | ||
205 | $request = Request::createFromEnvironment($env); | ||
206 | |||
207 | $this->controller->putTag($request, new Response(), ['tagName' => 'nopenope']); | ||
208 | } | ||
209 | } | ||