]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - tests/api/ApiUtilsTest.php
Compatibility with PHPUnit 9
[github/shaarli/Shaarli.git] / tests / api / ApiUtilsTest.php
1 <?php
2
3 namespace Shaarli\Api;
4
5 use Shaarli\Bookmark\Bookmark;
6 use Shaarli\Http\Base64Url;
7
8 /**
9 * Class ApiUtilsTest
10 */
11 class ApiUtilsTest extends \Shaarli\TestCase
12 {
13 /**
14 * Force the timezone for ISO datetimes.
15 */
16 public static function setUpBeforeClass(): void
17 {
18 date_default_timezone_set('UTC');
19 }
20
21 /**
22 * Generate a valid JWT token.
23 *
24 * @param string $secret API secret used to generate the signature.
25 *
26 * @return string Generated token.
27 */
28 public static function generateValidJwtToken($secret)
29 {
30 $header = Base64Url::encode('{
31 "typ": "JWT",
32 "alg": "HS512"
33 }');
34 $payload = Base64Url::encode('{
35 "iat": '. time() .'
36 }');
37 $signature = Base64Url::encode(hash_hmac('sha512', $header .'.'. $payload, $secret, true));
38 return $header .'.'. $payload .'.'. $signature;
39 }
40
41 /**
42 * Generate a JWT token from given header and payload.
43 *
44 * @param string $header Header in JSON format.
45 * @param string $payload Payload in JSON format.
46 * @param string $secret API secret used to hash the signature.
47 *
48 * @return string JWT token.
49 */
50 public static function generateCustomJwtToken($header, $payload, $secret)
51 {
52 $header = Base64Url::encode($header);
53 $payload = Base64Url::encode($payload);
54 $signature = Base64Url::encode(hash_hmac('sha512', $header . '.' . $payload, $secret, true));
55 return $header . '.' . $payload . '.' . $signature;
56 }
57
58 /**
59 * Test validateJwtToken() with a valid JWT token.
60 */
61 public function testValidateJwtTokenValid()
62 {
63 $secret = 'WarIsPeace';
64 $this->assertTrue(ApiUtils::validateJwtToken(self::generateValidJwtToken($secret), $secret));
65 }
66
67 /**
68 * Test validateJwtToken() with a malformed JWT token.
69 */
70 public function testValidateJwtTokenMalformed()
71 {
72 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
73 $this->expectExceptionMessage('Malformed JWT token');
74
75 $token = 'ABC.DEF';
76 ApiUtils::validateJwtToken($token, 'foo');
77 }
78
79 /**
80 * Test validateJwtToken() with an empty JWT token.
81 */
82 public function testValidateJwtTokenMalformedEmpty()
83 {
84 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
85 $this->expectExceptionMessage('Malformed JWT token');
86
87 $token = false;
88 ApiUtils::validateJwtToken($token, 'foo');
89 }
90
91 /**
92 * Test validateJwtToken() with a JWT token without header.
93 */
94 public function testValidateJwtTokenMalformedEmptyHeader()
95 {
96 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
97 $this->expectExceptionMessage('Malformed JWT token');
98
99 $token = '.payload.signature';
100 ApiUtils::validateJwtToken($token, 'foo');
101 }
102
103 /**
104 * Test validateJwtToken() with a JWT token without payload
105 */
106 public function testValidateJwtTokenMalformedEmptyPayload()
107 {
108 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
109 $this->expectExceptionMessage('Malformed JWT token');
110
111 $token = 'header..signature';
112 ApiUtils::validateJwtToken($token, 'foo');
113 }
114
115 /**
116 * Test validateJwtToken() with a JWT token with an empty signature.
117 */
118 public function testValidateJwtTokenInvalidSignatureEmpty()
119 {
120 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
121 $this->expectExceptionMessage('Invalid JWT signature');
122
123 $token = 'header.payload.';
124 ApiUtils::validateJwtToken($token, 'foo');
125 }
126
127 /**
128 * Test validateJwtToken() with a JWT token with an invalid signature.
129 */
130 public function testValidateJwtTokenInvalidSignature()
131 {
132 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
133 $this->expectExceptionMessage('Invalid JWT signature');
134
135 $token = 'header.payload.nope';
136 ApiUtils::validateJwtToken($token, 'foo');
137 }
138
139 /**
140 * Test validateJwtToken() with a JWT token with a signature generated with the wrong API secret.
141 */
142 public function testValidateJwtTokenInvalidSignatureSecret()
143 {
144 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
145 $this->expectExceptionMessage('Invalid JWT signature');
146
147 ApiUtils::validateJwtToken(self::generateValidJwtToken('foo'), 'bar');
148 }
149
150 /**
151 * Test validateJwtToken() with a JWT token with a an invalid header (not JSON).
152 */
153 public function testValidateJwtTokenInvalidHeader()
154 {
155 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
156 $this->expectExceptionMessage('Invalid JWT header');
157
158 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret');
159 ApiUtils::validateJwtToken($token, 'secret');
160 }
161
162 /**
163 * Test validateJwtToken() with a JWT token with a an invalid payload (not JSON).
164 */
165 public function testValidateJwtTokenInvalidPayload()
166 {
167 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
168 $this->expectExceptionMessage('Invalid JWT payload');
169
170 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret');
171 ApiUtils::validateJwtToken($token, 'secret');
172 }
173
174 /**
175 * Test validateJwtToken() with a JWT token without issued time.
176 */
177 public function testValidateJwtTokenInvalidTimeEmpty()
178 {
179 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
180 $this->expectExceptionMessage('Invalid JWT issued time');
181
182 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret');
183 ApiUtils::validateJwtToken($token, 'secret');
184 }
185
186 /**
187 * Test validateJwtToken() with an expired JWT token.
188 */
189 public function testValidateJwtTokenInvalidTimeExpired()
190 {
191 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
192 $this->expectExceptionMessage('Invalid JWT issued time');
193
194 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret');
195 ApiUtils::validateJwtToken($token, 'secret');
196 }
197
198 /**
199 * Test validateJwtToken() with a JWT token issued in the future.
200 */
201 public function testValidateJwtTokenInvalidTimeFuture()
202 {
203 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
204 $this->expectExceptionMessage('Invalid JWT issued time');
205
206 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret');
207 ApiUtils::validateJwtToken($token, 'secret');
208 }
209
210 /**
211 * Test formatLink() with a link using all useful fields.
212 */
213 public function testFormatLinkComplete()
214 {
215 $indexUrl = 'https://domain.tld/sub/';
216 $data = [
217 'id' => 12,
218 'url' => 'http://lol.lol',
219 'shorturl' => 'abc',
220 'title' => 'Important Title',
221 'description' => 'It is very lol<tag>' . PHP_EOL . 'new line',
222 'tags' => 'blip .blop ',
223 'private' => '1',
224 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
225 'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'),
226 ];
227 $bookmark = new Bookmark();
228 $bookmark->fromArray($data);
229
230 $expected = [
231 'id' => 12,
232 'url' => 'http://lol.lol',
233 'shorturl' => 'abc',
234 'title' => 'Important Title',
235 'description' => 'It is very lol<tag>' . PHP_EOL . 'new line',
236 'tags' => ['blip', '.blop'],
237 'private' => true,
238 'created' => '2017-01-07T16:01:02+00:00',
239 'updated' => '2017-01-07T16:06:12+00:00',
240 ];
241
242 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
243 }
244
245 /**
246 * Test formatLink() with only minimal fields filled, and internal link.
247 */
248 public function testFormatLinkMinimalNote()
249 {
250 $indexUrl = 'https://domain.tld/sub/';
251 $data = [
252 'id' => 12,
253 'url' => '?abc',
254 'shorturl' => 'abc',
255 'title' => 'Note',
256 'description' => '',
257 'tags' => '',
258 'private' => '',
259 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
260 ];
261 $bookmark = new Bookmark();
262 $bookmark->fromArray($data);
263
264 $expected = [
265 'id' => 12,
266 'url' => 'https://domain.tld/sub/?abc',
267 'shorturl' => 'abc',
268 'title' => 'Note',
269 'description' => '',
270 'tags' => [],
271 'private' => false,
272 'created' => '2017-01-07T16:01:02+00:00',
273 'updated' => '',
274 ];
275
276 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
277 }
278
279 /**
280 * Test updateLink with valid data, and also unnecessary fields.
281 */
282 public function testUpdateLink()
283 {
284 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
285 $data = [
286 'id' => 12,
287 'url' => '?abc',
288 'shorturl' => 'abc',
289 'title' => 'Note',
290 'description' => '',
291 'tags' => '',
292 'private' => '',
293 'created' => $created,
294 ];
295 $old = new Bookmark();
296 $old->fromArray($data);
297
298 $data = [
299 'id' => 13,
300 'shorturl' => 'nope',
301 'url' => 'http://somewhere.else',
302 'title' => 'Le Cid',
303 'description' => 'Percé jusques au fond du cœur [...]',
304 'tags' => 'corneille rodrigue',
305 'private' => true,
306 'created' => 'creation',
307 'updated' => 'updation',
308 ];
309 $new = new Bookmark();
310 $new->fromArray($data);
311
312 $result = ApiUtils::updateLink($old, $new);
313 $this->assertEquals(12, $result->getId());
314 $this->assertEquals('http://somewhere.else', $result->getUrl());
315 $this->assertEquals('abc', $result->getShortUrl());
316 $this->assertEquals('Le Cid', $result->getTitle());
317 $this->assertEquals('Percé jusques au fond du cœur [...]', $result->getDescription());
318 $this->assertEquals('corneille rodrigue', $result->getTagsString());
319 $this->assertEquals(true, $result->isPrivate());
320 $this->assertEquals($created, $result->getCreated());
321 }
322
323 /**
324 * Test updateLink with minimal data.
325 */
326 public function testUpdateLinkMinimal()
327 {
328 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
329 $data = [
330 'id' => 12,
331 'url' => '?abc',
332 'shorturl' => 'abc',
333 'title' => 'Note',
334 'description' => 'Interesting description!',
335 'tags' => 'doggo',
336 'private' => true,
337 'created' => $created,
338 ];
339 $old = new Bookmark();
340 $old->fromArray($data);
341
342 $new = new Bookmark();
343
344 $result = ApiUtils::updateLink($old, $new);
345 $this->assertEquals(12, $result->getId());
346 $this->assertEquals('', $result->getUrl());
347 $this->assertEquals('abc', $result->getShortUrl());
348 $this->assertEquals('', $result->getTitle());
349 $this->assertEquals('', $result->getDescription());
350 $this->assertEquals('', $result->getTagsString());
351 $this->assertEquals(false, $result->isPrivate());
352 $this->assertEquals($created, $result->getCreated());
353 }
354 }