aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.htaccess7
-rw-r--r--application/api/ApiUtils.php16
-rw-r--r--application/api/controllers/Links.php8
-rw-r--r--application/api/controllers/Tags.php161
-rw-r--r--application/api/exceptions/ApiTagNotFoundException.php32
-rw-r--r--doc/md/docker/reverse-proxy-configuration.md7
-rw-r--r--index.php6
-rw-r--r--tests/api/controllers/history/HistoryTest.php (renamed from tests/api/controllers/HistoryTest.php)0
-rw-r--r--tests/api/controllers/info/InfoTest.php (renamed from tests/api/controllers/InfoTest.php)0
-rw-r--r--tests/api/controllers/links/DeleteLinkTest.php (renamed from tests/api/controllers/DeleteLinkTest.php)0
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php (renamed from tests/api/controllers/GetLinkIdTest.php)0
-rw-r--r--tests/api/controllers/links/GetLinksTest.php (renamed from tests/api/controllers/GetLinksTest.php)0
-rw-r--r--tests/api/controllers/links/PostLinkTest.php (renamed from tests/api/controllers/PostLinkTest.php)0
-rw-r--r--tests/api/controllers/links/PutLinkTest.php (renamed from tests/api/controllers/PutLinkTest.php)0
-rw-r--r--tests/api/controllers/tags/DeleteTagTest.php164
-rw-r--r--tests/api/controllers/tags/GetTagNameTest.php129
-rw-r--r--tests/api/controllers/tags/GetTagsTest.php209
-rw-r--r--tests/api/controllers/tags/PutTagTest.php209
18 files changed, 942 insertions, 6 deletions
diff --git a/.htaccess b/.htaccess
index 7ba4744b..b238854c 100644
--- a/.htaccess
+++ b/.htaccess
@@ -14,3 +14,10 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
14RewriteCond %{REQUEST_FILENAME} !-f 14RewriteCond %{REQUEST_FILENAME} !-f
15RewriteCond %{REQUEST_FILENAME} !-d 15RewriteCond %{REQUEST_FILENAME} !-d
16RewriteRule ^ index.php [QSA,L] 16RewriteRule ^ 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
3namespace Shaarli\Api\Controllers;
4
5use Shaarli\Api\ApiUtils;
6use Shaarli\Api\Exceptions\ApiBadParametersException;
7use Shaarli\Api\Exceptions\ApiLinkNotFoundException;
8use Shaarli\Api\Exceptions\ApiTagNotFoundException;
9use Slim\Http\Request;
10use 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 */
19class 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
3namespace Shaarli\Api\Exceptions;
4
5
6use 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 */
15class 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
16The following HTTP headers are set by using the `ProxyPass` directive: 16The 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
22The 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>
diff --git a/index.php b/index.php
index ddd5dbf5..29d67f62 100644
--- a/index.php
+++ b/index.php
@@ -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
4namespace Shaarli\Api\Controllers;
5
6use Shaarli\Config\ConfigManager;
7use Slim\Container;
8use Slim\Http\Environment;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class 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
3namespace Shaarli\Api\Controllers;
4
5use Shaarli\Config\ConfigManager;
6
7use Slim\Container;
8use Slim\Http\Environment;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class GetTagNameTest
14 *
15 * Test getTag by tag name API service.
16 *
17 * @package Shaarli\Api\Controllers
18 */
19class 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
2namespace Shaarli\Api\Controllers;
3
4use Shaarli\Config\ConfigManager;
5
6use Slim\Container;
7use Slim\Http\Environment;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class GetTagsTest
13 *
14 * Test get tag list REST API service.
15 *
16 * @package Shaarli\Api\Controllers
17 */
18class 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
4namespace Shaarli\Api\Controllers;
5
6
7use Shaarli\Api\Exceptions\ApiBadParametersException;
8use Shaarli\Config\ConfigManager;
9use Slim\Container;
10use Slim\Http\Environment;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class 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}