ApiUtilsTest.php 11 KB

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