]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - tests/api/ApiUtilsTest.php
Compatibility with PHPUnit 9
[github/shaarli/Shaarli.git] / tests / api / ApiUtilsTest.php
CommitLineData
18e67967
A
1<?php
2
3namespace Shaarli\Api;
4
e26e2060 5use Shaarli\Bookmark\Bookmark;
00af48d9 6use Shaarli\Http\Base64Url;
7a9daac5 7
18e67967
A
8/**
9 * Class ApiUtilsTest
10 */
a5a9cf23 11class ApiUtilsTest extends \Shaarli\TestCase
18e67967
A
12{
13 /**
14 * Force the timezone for ISO datetimes.
15 */
8f60e120 16 public static function setUpBeforeClass(): void
18e67967
A
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 {
7a9daac5 30 $header = Base64Url::encode('{
18e67967
A
31 "typ": "JWT",
32 "alg": "HS512"
33 }');
7a9daac5 34 $payload = Base64Url::encode('{
18e67967
A
35 "iat": '. time() .'
36 }');
067c2dd8 37 $signature = Base64Url::encode(hash_hmac('sha512', $header .'.'. $payload, $secret, true));
18e67967
A
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 {
7a9daac5
V
52 $header = Base64Url::encode($header);
53 $payload = Base64Url::encode($payload);
54 $signature = Base64Url::encode(hash_hmac('sha512', $header . '.' . $payload, $secret, true));
18e67967
A
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';
def39d0d 64 $this->assertTrue(ApiUtils::validateJwtToken(self::generateValidJwtToken($secret), $secret));
18e67967
A
65 }
66
67 /**
68 * Test validateJwtToken() with a malformed JWT token.
18e67967
A
69 */
70 public function testValidateJwtTokenMalformed()
71 {
b1baca99
A
72 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
73 $this->expectExceptionMessage('Malformed JWT token');
74
18e67967
A
75 $token = 'ABC.DEF';
76 ApiUtils::validateJwtToken($token, 'foo');
77 }
78
79 /**
80 * Test validateJwtToken() with an empty JWT token.
18e67967
A
81 */
82 public function testValidateJwtTokenMalformedEmpty()
83 {
b1baca99
A
84 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
85 $this->expectExceptionMessage('Malformed JWT token');
86
18e67967
A
87 $token = false;
88 ApiUtils::validateJwtToken($token, 'foo');
89 }
90
91 /**
92 * Test validateJwtToken() with a JWT token without header.
18e67967
A
93 */
94 public function testValidateJwtTokenMalformedEmptyHeader()
95 {
b1baca99
A
96 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
97 $this->expectExceptionMessage('Malformed JWT token');
98
18e67967
A
99 $token = '.payload.signature';
100 ApiUtils::validateJwtToken($token, 'foo');
101 }
102
103 /**
104 * Test validateJwtToken() with a JWT token without payload
18e67967
A
105 */
106 public function testValidateJwtTokenMalformedEmptyPayload()
107 {
b1baca99
A
108 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
109 $this->expectExceptionMessage('Malformed JWT token');
110
18e67967
A
111 $token = 'header..signature';
112 ApiUtils::validateJwtToken($token, 'foo');
113 }
114
115 /**
116 * Test validateJwtToken() with a JWT token with an empty signature.
18e67967
A
117 */
118 public function testValidateJwtTokenInvalidSignatureEmpty()
119 {
b1baca99
A
120 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
121 $this->expectExceptionMessage('Invalid JWT signature');
122
18e67967
A
123 $token = 'header.payload.';
124 ApiUtils::validateJwtToken($token, 'foo');
125 }
126
127 /**
128 * Test validateJwtToken() with a JWT token with an invalid signature.
18e67967
A
129 */
130 public function testValidateJwtTokenInvalidSignature()
131 {
b1baca99
A
132 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
133 $this->expectExceptionMessage('Invalid JWT signature');
134
18e67967
A
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.
18e67967
A
141 */
142 public function testValidateJwtTokenInvalidSignatureSecret()
143 {
b1baca99
A
144 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
145 $this->expectExceptionMessage('Invalid JWT signature');
146
18e67967
A
147 ApiUtils::validateJwtToken(self::generateValidJwtToken('foo'), 'bar');
148 }
149
150 /**
151 * Test validateJwtToken() with a JWT token with a an invalid header (not JSON).
18e67967
A
152 */
153 public function testValidateJwtTokenInvalidHeader()
154 {
b1baca99
A
155 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
156 $this->expectExceptionMessage('Invalid JWT header');
157
18e67967
A
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).
18e67967
A
164 */
165 public function testValidateJwtTokenInvalidPayload()
166 {
b1baca99
A
167 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
168 $this->expectExceptionMessage('Invalid JWT payload');
169
18e67967
A
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.
18e67967
A
176 */
177 public function testValidateJwtTokenInvalidTimeEmpty()
178 {
b1baca99
A
179 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
180 $this->expectExceptionMessage('Invalid JWT issued time');
181
18e67967
A
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.
18e67967
A
188 */
189 public function testValidateJwtTokenInvalidTimeExpired()
190 {
b1baca99
A
191 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
192 $this->expectExceptionMessage('Invalid JWT issued time');
193
18e67967
A
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.
18e67967
A
200 */
201 public function testValidateJwtTokenInvalidTimeFuture()
202 {
b1baca99
A
203 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
204 $this->expectExceptionMessage('Invalid JWT issued time');
205
18e67967
A
206 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret');
207 ApiUtils::validateJwtToken($token, 'secret');
208 }
c3b00963
A
209
210 /**
211 * Test formatLink() with a link using all useful fields.
212 */
213 public function testFormatLinkComplete()
214 {
215 $indexUrl = 'https://domain.tld/sub/';
e26e2060 216 $data = [
c3b00963
A
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 ];
e26e2060
A
227 $bookmark = new Bookmark();
228 $bookmark->fromArray($data);
c3b00963
A
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
e26e2060 242 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
c3b00963
A
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/';
e26e2060 251 $data = [
c3b00963
A
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 ];
e26e2060
A
261 $bookmark = new Bookmark();
262 $bookmark->fromArray($data);
c3b00963
A
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
e26e2060 276 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
c3b00963 277 }
cf9181dd
A
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');
e26e2060 285 $data = [
cf9181dd
A
286 'id' => 12,
287 'url' => '?abc',
288 'shorturl' => 'abc',
289 'title' => 'Note',
290 'description' => '',
291 'tags' => '',
292 'private' => '',
293 'created' => $created,
294 ];
e26e2060
A
295 $old = new Bookmark();
296 $old->fromArray($data);
cf9181dd 297
e26e2060 298 $data = [
cf9181dd
A
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 ];
e26e2060
A
309 $new = new Bookmark();
310 $new->fromArray($data);
cf9181dd
A
311
312 $result = ApiUtils::updateLink($old, $new);
e26e2060
A
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());
cf9181dd
A
321 }
322
323 /**
324 * Test updateLink with minimal data.
325 */
326 public function testUpdateLinkMinimal()
327 {
328 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
e26e2060 329 $data = [
cf9181dd
A
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 ];
e26e2060
A
339 $old = new Bookmark();
340 $old->fromArray($data);
cf9181dd 341
e26e2060 342 $new = new Bookmark();
cf9181dd
A
343
344 $result = ApiUtils::updateLink($old, $new);
e26e2060
A
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());
cf9181dd 353 }
18e67967 354}