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