]>
Commit | Line | Data |
---|---|---|
18e67967 A |
1 | <?php |
2 | ||
3 | namespace Shaarli\Api; | |
4 | ||
7a9daac5 V |
5 | use Shaarli\Base64Url; |
6 | ||
7 | ||
18e67967 A |
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 | { | |
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 | }'); | |
7a9daac5 | 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'; | |
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 | } | |
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/'; | |
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 | } | |
18e67967 | 274 | } |