diff options
Diffstat (limited to 'tests/api/ApiUtilsTest.php')
-rw-r--r-- | tests/api/ApiUtilsTest.php | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/tests/api/ApiUtilsTest.php b/tests/api/ApiUtilsTest.php new file mode 100644 index 00000000..62baf4c5 --- /dev/null +++ b/tests/api/ApiUtilsTest.php | |||
@@ -0,0 +1,352 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Api; | ||
4 | |||
5 | use Shaarli\Base64Url; | ||
6 | |||
7 | |||
8 | /** | ||
9 | * Class ApiUtilsTest | ||
10 | */ | ||
11 | class ApiUtilsTest extends \PHPUnit_Framework_TestCase | ||
12 | { | ||
13 | /** | ||
14 | * Force the timezone for ISO datetimes. | ||
15 | */ | ||
16 | public static function setUpBeforeClass() | ||
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 | ApiUtils::validateJwtToken(self::generateValidJwtToken($secret), $secret); | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * Test validateJwtToken() with a malformed JWT token. | ||
69 | * | ||
70 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
71 | * @expectedExceptionMessage Malformed JWT token | ||
72 | */ | ||
73 | public function testValidateJwtTokenMalformed() | ||
74 | { | ||
75 | $token = 'ABC.DEF'; | ||
76 | ApiUtils::validateJwtToken($token, 'foo'); | ||
77 | } | ||
78 | |||
79 | /** | ||
80 | * Test validateJwtToken() with an empty JWT token. | ||
81 | * | ||
82 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
83 | * @expectedExceptionMessage Malformed JWT token | ||
84 | */ | ||
85 | public function testValidateJwtTokenMalformedEmpty() | ||
86 | { | ||
87 | $token = false; | ||
88 | ApiUtils::validateJwtToken($token, 'foo'); | ||
89 | } | ||
90 | |||
91 | /** | ||
92 | * Test validateJwtToken() with a JWT token without header. | ||
93 | * | ||
94 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
95 | * @expectedExceptionMessage Malformed JWT token | ||
96 | */ | ||
97 | public function testValidateJwtTokenMalformedEmptyHeader() | ||
98 | { | ||
99 | $token = '.payload.signature'; | ||
100 | ApiUtils::validateJwtToken($token, 'foo'); | ||
101 | } | ||
102 | |||
103 | /** | ||
104 | * Test validateJwtToken() with a JWT token without payload | ||
105 | * | ||
106 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
107 | * @expectedExceptionMessage Malformed JWT token | ||
108 | */ | ||
109 | public function testValidateJwtTokenMalformedEmptyPayload() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
119 | * @expectedExceptionMessage Invalid JWT signature | ||
120 | */ | ||
121 | public function testValidateJwtTokenInvalidSignatureEmpty() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
131 | * @expectedExceptionMessage Invalid JWT signature | ||
132 | */ | ||
133 | public function testValidateJwtTokenInvalidSignature() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
143 | * @expectedExceptionMessage Invalid JWT signature | ||
144 | */ | ||
145 | public function testValidateJwtTokenInvalidSignatureSecret() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
154 | * @expectedExceptionMessage Invalid JWT header | ||
155 | */ | ||
156 | public function testValidateJwtTokenInvalidHeader() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
166 | * @expectedExceptionMessage Invalid JWT payload | ||
167 | */ | ||
168 | public function testValidateJwtTokenInvalidPayload() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
178 | * @expectedExceptionMessage Invalid JWT issued time | ||
179 | */ | ||
180 | public function testValidateJwtTokenInvalidTimeEmpty() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
190 | * @expectedExceptionMessage Invalid JWT issued time | ||
191 | */ | ||
192 | public function testValidateJwtTokenInvalidTimeExpired() | ||
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 | * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException | ||
202 | * @expectedExceptionMessage Invalid JWT issued time | ||
203 | */ | ||
204 | public function testValidateJwtTokenInvalidTimeFuture() | ||
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 | $link = [ | ||
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 | |||
228 | $expected = [ | ||
229 | 'id' => 12, | ||
230 | 'url' => 'http://lol.lol', | ||
231 | 'shorturl' => 'abc', | ||
232 | 'title' => 'Important Title', | ||
233 | 'description' => 'It is very lol<tag>' . PHP_EOL . 'new line', | ||
234 | 'tags' => ['blip', '.blop'], | ||
235 | 'private' => true, | ||
236 | 'created' => '2017-01-07T16:01:02+00:00', | ||
237 | 'updated' => '2017-01-07T16:06:12+00:00', | ||
238 | ]; | ||
239 | |||
240 | $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); | ||
241 | } | ||
242 | |||
243 | /** | ||
244 | * Test formatLink() with only minimal fields filled, and internal link. | ||
245 | */ | ||
246 | public function testFormatLinkMinimalNote() | ||
247 | { | ||
248 | $indexUrl = 'https://domain.tld/sub/'; | ||
249 | $link = [ | ||
250 | 'id' => 12, | ||
251 | 'url' => '?abc', | ||
252 | 'shorturl' => 'abc', | ||
253 | 'title' => 'Note', | ||
254 | 'description' => '', | ||
255 | 'tags' => '', | ||
256 | 'private' => '', | ||
257 | 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), | ||
258 | ]; | ||
259 | |||
260 | $expected = [ | ||
261 | 'id' => 12, | ||
262 | 'url' => 'https://domain.tld/sub/?abc', | ||
263 | 'shorturl' => 'abc', | ||
264 | 'title' => 'Note', | ||
265 | 'description' => '', | ||
266 | 'tags' => [], | ||
267 | 'private' => false, | ||
268 | 'created' => '2017-01-07T16:01:02+00:00', | ||
269 | 'updated' => '', | ||
270 | ]; | ||
271 | |||
272 | $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); | ||
273 | } | ||
274 | |||
275 | /** | ||
276 | * Test updateLink with valid data, and also unnecessary fields. | ||
277 | */ | ||
278 | public function testUpdateLink() | ||
279 | { | ||
280 | $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); | ||
281 | $old = [ | ||
282 | 'id' => 12, | ||
283 | 'url' => '?abc', | ||
284 | 'shorturl' => 'abc', | ||
285 | 'title' => 'Note', | ||
286 | 'description' => '', | ||
287 | 'tags' => '', | ||
288 | 'private' => '', | ||
289 | 'created' => $created, | ||
290 | ]; | ||
291 | |||
292 | $new = [ | ||
293 | 'id' => 13, | ||
294 | 'shorturl' => 'nope', | ||
295 | 'url' => 'http://somewhere.else', | ||
296 | 'title' => 'Le Cid', | ||
297 | 'description' => 'Percé jusques au fond du cœur [...]', | ||
298 | 'tags' => 'corneille rodrigue', | ||
299 | 'private' => true, | ||
300 | 'created' => 'creation', | ||
301 | 'updated' => 'updation', | ||
302 | ]; | ||
303 | |||
304 | $result = ApiUtils::updateLink($old, $new); | ||
305 | $this->assertEquals(12, $result['id']); | ||
306 | $this->assertEquals('http://somewhere.else', $result['url']); | ||
307 | $this->assertEquals('abc', $result['shorturl']); | ||
308 | $this->assertEquals('Le Cid', $result['title']); | ||
309 | $this->assertEquals('Percé jusques au fond du cœur [...]', $result['description']); | ||
310 | $this->assertEquals('corneille rodrigue', $result['tags']); | ||
311 | $this->assertEquals(true, $result['private']); | ||
312 | $this->assertEquals($created, $result['created']); | ||
313 | $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']); | ||
314 | } | ||
315 | |||
316 | /** | ||
317 | * Test updateLink with minimal data. | ||
318 | */ | ||
319 | public function testUpdateLinkMinimal() | ||
320 | { | ||
321 | $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); | ||
322 | $old = [ | ||
323 | 'id' => 12, | ||
324 | 'url' => '?abc', | ||
325 | 'shorturl' => 'abc', | ||
326 | 'title' => 'Note', | ||
327 | 'description' => 'Interesting description!', | ||
328 | 'tags' => 'doggo', | ||
329 | 'private' => true, | ||
330 | 'created' => $created, | ||
331 | ]; | ||
332 | |||
333 | $new = [ | ||
334 | 'url' => '', | ||
335 | 'title' => '', | ||
336 | 'description' => '', | ||
337 | 'tags' => '', | ||
338 | 'private' => false, | ||
339 | ]; | ||
340 | |||
341 | $result = ApiUtils::updateLink($old, $new); | ||
342 | $this->assertEquals(12, $result['id']); | ||
343 | $this->assertEquals('?abc', $result['url']); | ||
344 | $this->assertEquals('abc', $result['shorturl']); | ||
345 | $this->assertEquals('?abc', $result['title']); | ||
346 | $this->assertEquals('', $result['description']); | ||
347 | $this->assertEquals('', $result['tags']); | ||
348 | $this->assertEquals(false, $result['private']); | ||
349 | $this->assertEquals($created, $result['created']); | ||
350 | $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']); | ||
351 | } | ||
352 | } | ||