5 use Shaarli\Http\Base64Url
;
10 class ApiUtilsTest
extends \PHPUnit\Framework\TestCase
13 * Force the timezone for ISO datetimes.
15 public static function setUpBeforeClass()
17 date_default_timezone_set('UTC');
21 * Generate a valid JWT token.
23 * @param string $secret API secret used to generate the signature.
25 * @return string Generated token.
27 public static function generateValidJwtToken($secret)
29 $header = Base64Url
::encode('{
33 $payload = Base64Url
::encode('{
36 $signature = Base64Url
::encode(hash_hmac('sha512', $header .'.'. $payload, $secret, true));
37 return $header .'.'. $payload .'.'. $signature;
41 * Generate a JWT token from given header and payload.
43 * @param string $header Header in JSON format.
44 * @param string $payload Payload in JSON format.
45 * @param string $secret API secret used to hash the signature.
47 * @return string JWT token.
49 public static function generateCustomJwtToken($header, $payload, $secret)
51 $header = Base64Url
::encode($header);
52 $payload = Base64Url
::encode($payload);
53 $signature = Base64Url
::encode(hash_hmac('sha512', $header . '.' . $payload, $secret, true));
54 return $header . '.' . $payload . '.' . $signature;
58 * Test validateJwtToken() with a valid JWT token.
60 public function testValidateJwtTokenValid()
62 $secret = 'WarIsPeace';
63 ApiUtils
::validateJwtToken(self
::generateValidJwtToken($secret), $secret);
67 * Test validateJwtToken() with a malformed JWT token.
69 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
70 * @expectedExceptionMessage Malformed JWT token
72 public function testValidateJwtTokenMalformed()
75 ApiUtils
::validateJwtToken($token, 'foo');
79 * Test validateJwtToken() with an empty JWT token.
81 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
82 * @expectedExceptionMessage Malformed JWT token
84 public function testValidateJwtTokenMalformedEmpty()
87 ApiUtils
::validateJwtToken($token, 'foo');
91 * Test validateJwtToken() with a JWT token without header.
93 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
94 * @expectedExceptionMessage Malformed JWT token
96 public function testValidateJwtTokenMalformedEmptyHeader()
98 $token = '.payload.signature';
99 ApiUtils
::validateJwtToken($token, 'foo');
103 * Test validateJwtToken() with a JWT token without payload
105 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
106 * @expectedExceptionMessage Malformed JWT token
108 public function testValidateJwtTokenMalformedEmptyPayload()
110 $token = 'header..signature';
111 ApiUtils
::validateJwtToken($token, 'foo');
115 * Test validateJwtToken() with a JWT token with an empty signature.
117 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
118 * @expectedExceptionMessage Invalid JWT signature
120 public function testValidateJwtTokenInvalidSignatureEmpty()
122 $token = 'header.payload.';
123 ApiUtils
::validateJwtToken($token, 'foo');
127 * Test validateJwtToken() with a JWT token with an invalid signature.
129 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
130 * @expectedExceptionMessage Invalid JWT signature
132 public function testValidateJwtTokenInvalidSignature()
134 $token = 'header.payload.nope';
135 ApiUtils
::validateJwtToken($token, 'foo');
139 * Test validateJwtToken() with a JWT token with a signature generated with the wrong API secret.
141 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
142 * @expectedExceptionMessage Invalid JWT signature
144 public function testValidateJwtTokenInvalidSignatureSecret()
146 ApiUtils
::validateJwtToken(self
::generateValidJwtToken('foo'), 'bar');
150 * Test validateJwtToken() with a JWT token with a an invalid header (not JSON).
152 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
153 * @expectedExceptionMessage Invalid JWT header
155 public function testValidateJwtTokenInvalidHeader()
157 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret');
158 ApiUtils
::validateJwtToken($token, 'secret');
162 * Test validateJwtToken() with a JWT token with a an invalid payload (not JSON).
164 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
165 * @expectedExceptionMessage Invalid JWT payload
167 public function testValidateJwtTokenInvalidPayload()
169 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret');
170 ApiUtils
::validateJwtToken($token, 'secret');
174 * Test validateJwtToken() with a JWT token without issued time.
176 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
177 * @expectedExceptionMessage Invalid JWT issued time
179 public function testValidateJwtTokenInvalidTimeEmpty()
181 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret');
182 ApiUtils
::validateJwtToken($token, 'secret');
186 * Test validateJwtToken() with an expired JWT token.
188 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
189 * @expectedExceptionMessage Invalid JWT issued time
191 public function testValidateJwtTokenInvalidTimeExpired()
193 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret');
194 ApiUtils
::validateJwtToken($token, 'secret');
198 * Test validateJwtToken() with a JWT token issued in the future.
200 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
201 * @expectedExceptionMessage Invalid JWT issued time
203 public function testValidateJwtTokenInvalidTimeFuture()
205 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret');
206 ApiUtils
::validateJwtToken($token, 'secret');
210 * Test formatLink() with a link using all useful fields.
212 public function testFormatLinkComplete()
214 $indexUrl = 'https://domain.tld/sub/';
217 'url' => 'http://lol.lol',
219 'title' => 'Important Title',
220 'description' => 'It is very lol<tag>' . PHP_EOL
. 'new line',
221 'tags' => 'blip .blop ',
223 'created' => \DateTime
::createFromFormat('Ymd_His', '20170107_160102'),
224 'updated' => \DateTime
::createFromFormat('Ymd_His', '20170107_160612'),
229 'url' => 'http://lol.lol',
231 'title' => 'Important Title',
232 'description' => 'It is very lol<tag>' . PHP_EOL
. 'new line',
233 'tags' => ['blip', '.blop'],
235 'created' => '2017-01-07T16:01:02+00:00',
236 'updated' => '2017-01-07T16:06:12+00:00',
239 $this->assertEquals($expected, ApiUtils
::formatLink($link, $indexUrl));
243 * Test formatLink() with only minimal fields filled, and internal link.
245 public function testFormatLinkMinimalNote()
247 $indexUrl = 'https://domain.tld/sub/';
256 'created' => \DateTime
::createFromFormat('Ymd_His', '20170107_160102'),
261 'url' => 'https://domain.tld/sub/?abc',
267 'created' => '2017-01-07T16:01:02+00:00',
271 $this->assertEquals($expected, ApiUtils
::formatLink($link, $indexUrl));
275 * Test updateLink with valid data, and also unnecessary fields.
277 public function testUpdateLink()
279 $created = \DateTime
::createFromFormat('Ymd_His', '20170107_160102');
288 'created' => $created,
293 'shorturl' => 'nope',
294 'url' => 'http://somewhere.else',
296 'description' => 'Percé jusques au fond du cœur [...]',
297 'tags' => 'corneille rodrigue',
299 'created' => 'creation',
300 'updated' => 'updation',
303 $result = ApiUtils
::updateLink($old, $new);
304 $this->assertEquals(12, $result['id']);
305 $this->assertEquals('http://somewhere.else', $result['url']);
306 $this->assertEquals('abc', $result['shorturl']);
307 $this->assertEquals('Le Cid', $result['title']);
308 $this->assertEquals('Percé jusques au fond du cœur [...]', $result['description']);
309 $this->assertEquals('corneille rodrigue', $result['tags']);
310 $this->assertEquals(true, $result['private']);
311 $this->assertEquals($created, $result['created']);
312 $this->assertTrue(new \
DateTime('5 seconds ago') < $result['updated']);
316 * Test updateLink with minimal data.
318 public function testUpdateLinkMinimal()
320 $created = \DateTime
::createFromFormat('Ymd_His', '20170107_160102');
326 'description' => 'Interesting description!',
329 'created' => $created,
340 $result = ApiUtils
::updateLink($old, $new);
341 $this->assertEquals(12, $result['id']);
342 $this->assertEquals('?abc', $result['url']);
343 $this->assertEquals('abc', $result['shorturl']);
344 $this->assertEquals('?abc', $result['title']);
345 $this->assertEquals('', $result['description']);
346 $this->assertEquals('', $result['tags']);
347 $this->assertEquals(false, $result['private']);
348 $this->assertEquals($created, $result['created']);
349 $this->assertTrue(new \
DateTime('5 seconds ago') < $result['updated']);