5 use Shaarli\Bookmark\Bookmark
;
6 use Shaarli\Http\Base64Url
;
11 class ApiUtilsTest
extends \Shaarli\TestCase
14 * Force the timezone for ISO datetimes.
16 public static function setUpBeforeClass(): void
18 date_default_timezone_set('UTC');
22 * Generate a valid JWT token.
24 * @param string $secret API secret used to generate the signature.
26 * @return string Generated token.
28 public static function generateValidJwtToken($secret)
30 $header = Base64Url
::encode('{
34 $payload = Base64Url
::encode('{
37 $signature = Base64Url
::encode(hash_hmac('sha512', $header .'.'. $payload, $secret, true));
38 return $header .'.'. $payload .'.'. $signature;
42 * Generate a JWT token from given header and payload.
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.
48 * @return string JWT token.
50 public static function generateCustomJwtToken($header, $payload, $secret)
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;
59 * Test validateJwtToken() with a valid JWT token.
61 public function testValidateJwtTokenValid()
63 $secret = 'WarIsPeace';
64 $this->assertTrue(ApiUtils
::validateJwtToken(self
::generateValidJwtToken($secret), $secret));
68 * Test validateJwtToken() with a malformed JWT token.
70 public function testValidateJwtTokenMalformed()
72 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
73 $this->expectExceptionMessage('Malformed JWT token');
76 ApiUtils
::validateJwtToken($token, 'foo');
80 * Test validateJwtToken() with an empty JWT token.
82 public function testValidateJwtTokenMalformedEmpty()
84 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
85 $this->expectExceptionMessage('Malformed JWT token');
88 ApiUtils
::validateJwtToken($token, 'foo');
92 * Test validateJwtToken() with a JWT token without header.
94 public function testValidateJwtTokenMalformedEmptyHeader()
96 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
97 $this->expectExceptionMessage('Malformed JWT token');
99 $token = '.payload.signature';
100 ApiUtils
::validateJwtToken($token, 'foo');
104 * Test validateJwtToken() with a JWT token without payload
106 public function testValidateJwtTokenMalformedEmptyPayload()
108 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
109 $this->expectExceptionMessage('Malformed JWT token');
111 $token = 'header..signature';
112 ApiUtils
::validateJwtToken($token, 'foo');
116 * Test validateJwtToken() with a JWT token with an empty signature.
118 public function testValidateJwtTokenInvalidSignatureEmpty()
120 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
121 $this->expectExceptionMessage('Invalid JWT signature');
123 $token = 'header.payload.';
124 ApiUtils
::validateJwtToken($token, 'foo');
128 * Test validateJwtToken() with a JWT token with an invalid signature.
130 public function testValidateJwtTokenInvalidSignature()
132 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
133 $this->expectExceptionMessage('Invalid JWT signature');
135 $token = 'header.payload.nope';
136 ApiUtils
::validateJwtToken($token, 'foo');
140 * Test validateJwtToken() with a JWT token with a signature generated with the wrong API secret.
142 public function testValidateJwtTokenInvalidSignatureSecret()
144 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
145 $this->expectExceptionMessage('Invalid JWT signature');
147 ApiUtils
::validateJwtToken(self
::generateValidJwtToken('foo'), 'bar');
151 * Test validateJwtToken() with a JWT token with a an invalid header (not JSON).
153 public function testValidateJwtTokenInvalidHeader()
155 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
156 $this->expectExceptionMessage('Invalid JWT header');
158 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret');
159 ApiUtils
::validateJwtToken($token, 'secret');
163 * Test validateJwtToken() with a JWT token with a an invalid payload (not JSON).
165 public function testValidateJwtTokenInvalidPayload()
167 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
168 $this->expectExceptionMessage('Invalid JWT payload');
170 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret');
171 ApiUtils
::validateJwtToken($token, 'secret');
175 * Test validateJwtToken() with a JWT token without issued time.
177 public function testValidateJwtTokenInvalidTimeEmpty()
179 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
180 $this->expectExceptionMessage('Invalid JWT issued time');
182 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret');
183 ApiUtils
::validateJwtToken($token, 'secret');
187 * Test validateJwtToken() with an expired JWT token.
189 public function testValidateJwtTokenInvalidTimeExpired()
191 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
192 $this->expectExceptionMessage('Invalid JWT issued time');
194 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret');
195 ApiUtils
::validateJwtToken($token, 'secret');
199 * Test validateJwtToken() with a JWT token issued in the future.
201 public function testValidateJwtTokenInvalidTimeFuture()
203 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException
::class);
204 $this->expectExceptionMessage('Invalid JWT issued time');
206 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret');
207 ApiUtils
::validateJwtToken($token, 'secret');
211 * Test formatLink() with a link using all useful fields.
213 public function testFormatLinkComplete()
215 $indexUrl = 'https://domain.tld/sub/';
218 'url' => 'http://lol.lol',
220 'title' => 'Important Title',
221 'description' => 'It is very lol<tag>' . PHP_EOL
. 'new line',
222 'tags' => 'blip .blop ',
224 'created' => \DateTime
::createFromFormat('Ymd_His', '20170107_160102'),
225 'updated' => \DateTime
::createFromFormat('Ymd_His', '20170107_160612'),
227 $bookmark = new Bookmark();
228 $bookmark->fromArray($data);
232 'url' => 'http://lol.lol',
234 'title' => 'Important Title',
235 'description' => 'It is very lol<tag>' . PHP_EOL
. 'new line',
236 'tags' => ['blip', '.blop'],
238 'created' => '2017-01-07T16:01:02+00:00',
239 'updated' => '2017-01-07T16:06:12+00:00',
242 $this->assertEquals($expected, ApiUtils
::formatLink($bookmark, $indexUrl));
246 * Test formatLink() with only minimal fields filled, and internal link.
248 public function testFormatLinkMinimalNote()
250 $indexUrl = 'https://domain.tld/sub/';
259 'created' => \DateTime
::createFromFormat('Ymd_His', '20170107_160102'),
261 $bookmark = new Bookmark();
262 $bookmark->fromArray($data);
266 'url' => 'https://domain.tld/sub/?abc',
272 'created' => '2017-01-07T16:01:02+00:00',
276 $this->assertEquals($expected, ApiUtils
::formatLink($bookmark, $indexUrl));
280 * Test updateLink with valid data, and also unnecessary fields.
282 public function testUpdateLink()
284 $created = \DateTime
::createFromFormat('Ymd_His', '20170107_160102');
293 'created' => $created,
295 $old = new Bookmark();
296 $old->fromArray($data);
300 'shorturl' => 'nope',
301 'url' => 'http://somewhere.else',
303 'description' => 'Percé jusques au fond du cœur [...]',
304 'tags' => 'corneille rodrigue',
306 'created' => 'creation',
307 'updated' => 'updation',
309 $new = new Bookmark();
310 $new->fromArray($data);
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());
324 * Test updateLink with minimal data.
326 public function testUpdateLinkMinimal()
328 $created = \DateTime
::createFromFormat('Ymd_His', '20170107_160102');
334 'description' => 'Interesting description!',
337 'created' => $created,
339 $old = new Bookmark();
340 $old->fromArray($data);
342 $new = new Bookmark();
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());