CookieTest.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <?php
  2. /**
  3. * Tests the cookie class
  4. *
  5. * @group ko7
  6. * @group ko7.core
  7. * @group ko7.core.cookie
  8. *
  9. * @package KO7
  10. * @category Tests
  11. *
  12. * @author Jeremy Bush <contractfrombelow@gmail.com>
  13. * @author Andrew Coulton <andrew@ingenerator.com>
  14. * @copyright (c) 2007-2016 Kohana Team
  15. * @copyright (c) since 2016 Koseven Team
  16. * @license https://koseven.dev/LICENSE
  17. */
  18. class KO7_CookieTest extends Unittest_TestCase
  19. {
  20. const UNIX_TIMESTAMP = 1411040141;
  21. const COOKIE_EXPIRATION = 60;
  22. /**
  23. * Sets up the environment
  24. */
  25. // @codingStandardsIgnoreStart
  26. public function setUp(): void
  27. // @codingStandardsIgnoreEnd
  28. {
  29. parent::setUp();
  30. KO7_CookieTest_TestableCookie::$_mock_cookies_set = [];
  31. $this->setEnvironment([
  32. 'Cookie::$salt' => 'some-random-salt',
  33. 'HTTP_USER_AGENT' => 'cli'
  34. ]);
  35. }
  36. /**
  37. * Tests that cookies are set with the global path, domain, etc options.
  38. *
  39. * @covers Cookie::set
  40. */
  41. public function test_set_creates_cookie_with_configured_cookie_options()
  42. {
  43. $this->setEnvironment([
  44. 'Cookie::$path' => '/path',
  45. 'Cookie::$domain' => 'my.domain',
  46. 'Cookie::$secure' => TRUE,
  47. 'Cookie::$httponly' => FALSE,
  48. ]);
  49. KO7_CookieTest_TestableCookie::set('cookie', 'value');
  50. $this->assertSetCookieWith([
  51. 'path' => '/path',
  52. 'domain' => 'my.domain',
  53. 'secure' => TRUE,
  54. 'httponly' => FALSE
  55. ]);
  56. }
  57. /**
  58. * Provider for test_set_calculates_expiry_from_lifetime
  59. *
  60. * @return array of $lifetime, $expect_expiry
  61. */
  62. public function provider_set_calculates_expiry_from_lifetime()
  63. {
  64. return [
  65. [NULL, self::COOKIE_EXPIRATION + self::UNIX_TIMESTAMP],
  66. [0, 0],
  67. [10, 10 + self::UNIX_TIMESTAMP],
  68. ];
  69. }
  70. /**
  71. * @param int $expiration
  72. * @param int $expect_expiry
  73. *
  74. * @dataProvider provider_set_calculates_expiry_from_lifetime
  75. * @covers Cookie::set
  76. */
  77. public function test_set_calculates_expiry_from_lifetime($expiration, $expect_expiry)
  78. {
  79. $this->setEnvironment(['Cookie::$expiration' => self::COOKIE_EXPIRATION]);
  80. KO7_CookieTest_TestableCookie::set('foo', 'bar', $expiration);
  81. $this->assertSetCookieWith(['expire' => $expect_expiry]);
  82. }
  83. /**
  84. * @covers Cookie::get
  85. */
  86. public function test_get_returns_default_if_cookie_missing()
  87. {
  88. unset($_COOKIE['missing_cookie']);
  89. $this->assertEquals('default', Cookie::get('missing_cookie', 'default'));
  90. }
  91. /**
  92. * @covers Cookie::get
  93. */
  94. public function test_get_returns_value_if_cookie_present_and_signed()
  95. {
  96. KO7_CookieTest_TestableCookie::set('cookie', 'value');
  97. $cookie = KO7_CookieTest_TestableCookie::$_mock_cookies_set[0];
  98. $_COOKIE[$cookie['name']] = $cookie['value'];
  99. $this->assertEquals('value', Cookie::get('cookie', 'default'));
  100. }
  101. /**
  102. * Provider for test_get_returns_default_without_deleting_if_cookie_unsigned
  103. *
  104. * @return array
  105. */
  106. public function provider_get_returns_default_without_deleting_if_cookie_unsigned()
  107. {
  108. return [
  109. ['unsalted'],
  110. ['un~salted'],
  111. ];
  112. }
  113. /**
  114. * Verifies that unsigned cookies are not available to the ko7 application, but are not affected for other
  115. * consumers.
  116. *
  117. * @param string $unsigned_value
  118. *
  119. * @dataProvider provider_get_returns_default_without_deleting_if_cookie_unsigned
  120. * @covers Cookie::get
  121. */
  122. public function test_get_returns_default_without_deleting_if_cookie_unsigned($unsigned_value)
  123. {
  124. $_COOKIE['cookie'] = $unsigned_value;
  125. $this->assertEquals('default', KO7_CookieTest_TestableCookie::get('cookie', 'default'));
  126. $this->assertEquals($unsigned_value, $_COOKIE['cookie'], '$_COOKIE not affected');
  127. $this->assertEmpty(KO7_CookieTest_TestableCookie::$_mock_cookies_set, 'No cookies set or changed');
  128. }
  129. /**
  130. * If a cookie looks like a signed cookie but the signature no longer matches, it should be deleted.
  131. *
  132. * @covers Cookie::get
  133. */
  134. public function test_get_returns_default_and_deletes_tampered_signed_cookie()
  135. {
  136. $_COOKIE['cookie'] = Cookie::salt('cookie', 'value').'~tampered';
  137. $this->assertEquals('default', KO7_CookieTest_TestableCookie::get('cookie', 'default'));
  138. $this->assertDeletedCookie('cookie');
  139. }
  140. /**
  141. * @covers Cookie::delete
  142. */
  143. public function test_delete_removes_cookie_from_globals_and_expires_cookie()
  144. {
  145. $_COOKIE['cookie'] = Cookie::salt('cookie', 'value').'~tampered';
  146. $this->assertTrue(KO7_CookieTest_TestableCookie::delete('cookie'));
  147. $this->assertDeletedCookie('cookie');
  148. }
  149. /**
  150. * @covers Cookie::delete
  151. * @link http://koseven.dev/issues/3501
  152. * @link http://koseven.dev/issues/3020
  153. */
  154. public function test_delete_does_not_require_configured_salt()
  155. {
  156. Cookie::$salt = NULL;
  157. $this->assertTrue(KO7_CookieTest_TestableCookie::delete('cookie'));
  158. $this->assertDeletedCookie('cookie');
  159. }
  160. /**
  161. * @covers Cookie::salt
  162. */
  163. public function test_salt_throws_with_no_configured_salt()
  164. {
  165. $this->expectException(KO7_Exception::class);
  166. Cookie::$salt = NULL;
  167. Cookie::salt('key', 'value');
  168. }
  169. /**
  170. * @covers Cookie::salt
  171. */
  172. public function test_salt_creates_same_hash_for_same_values_and_state()
  173. {
  174. $name = 'cookie';
  175. $value = 'value';
  176. $this->assertEquals(Cookie::salt($name, $value), Cookie::salt($name, $value));
  177. }
  178. /**
  179. * Provider for test_salt_creates_different_hash_for_different_data
  180. *
  181. * @return array
  182. */
  183. public function provider_salt_creates_different_hash_for_different_data()
  184. {
  185. return [
  186. [['name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'], ['name' => 'changed']],
  187. [['name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'], ['value' => 'changed']],
  188. [['name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'], ['salt' => 'changed-salt']]
  189. ];
  190. }
  191. /**
  192. * @param array $first_args
  193. * @param array $changed_args
  194. *
  195. * @dataProvider provider_salt_creates_different_hash_for_different_data
  196. * @covers Cookie::salt
  197. */
  198. public function test_salt_creates_different_hash_for_different_data($first_args, $changed_args)
  199. {
  200. $second_args = array_merge($first_args, $changed_args);
  201. $hashes = [];
  202. foreach ([$first_args, $second_args] as $args)
  203. {
  204. Cookie::$salt = $args['salt'];
  205. $this->set_or_remove_http_user_agent($args['user-agent']);
  206. $hashes[] = Cookie::salt($args['name'], $args['value']);
  207. }
  208. $this->assertNotEquals($hashes[0], $hashes[1]);
  209. }
  210. /**
  211. * Verify that a cookie was deleted from the global $_COOKIE array, and that a setcookie call was made to remove it
  212. * from the client.
  213. *
  214. * @param string $name
  215. */
  216. // @codingStandardsIgnoreStart
  217. protected function assertDeletedCookie($name)
  218. // @codingStandardsIgnoreEnd
  219. {
  220. $this->assertArrayNotHasKey($name, $_COOKIE);
  221. // To delete the client-side cookie, Cookie::delete should send a new cookie with value NULL and expiry in the past
  222. $this->assertSetCookieWith([
  223. 'name' => $name,
  224. 'value' => NULL,
  225. 'expire' => -86400,
  226. 'path' => Cookie::$path,
  227. 'domain' => Cookie::$domain,
  228. 'secure' => Cookie::$secure,
  229. 'httponly' => Cookie::$httponly
  230. ]);
  231. }
  232. /**
  233. * Verify that there was a single call to setcookie including the provided named arguments
  234. *
  235. * @param array $expected
  236. */
  237. // @codingStandardsIgnoreStart
  238. protected function assertSetCookieWith($expected)
  239. // @codingStandardsIgnoreEnd
  240. {
  241. $this->assertCount(1, KO7_CookieTest_TestableCookie::$_mock_cookies_set);
  242. $relevant_values = array_intersect_key(KO7_CookieTest_TestableCookie::$_mock_cookies_set[0], $expected);
  243. $this->assertEquals($expected, $relevant_values);
  244. }
  245. /**
  246. * Configure the $_SERVER[HTTP_USER_AGENT] environment variable for the test
  247. *
  248. * @param string $user_agent
  249. */
  250. protected function set_or_remove_http_user_agent($user_agent)
  251. {
  252. if ($user_agent === NULL)
  253. {
  254. unset($_SERVER['HTTP_USER_AGENT']);
  255. }
  256. else
  257. {
  258. $_SERVER['HTTP_USER_AGENT'] = $user_agent;
  259. }
  260. }
  261. }
  262. /**
  263. * Class KO7_CookieTest_TestableCookie wraps the cookie class to mock out the actual setcookie and time calls for
  264. * unit testing.
  265. */
  266. class KO7_CookieTest_TestableCookie extends Cookie {
  267. /**
  268. * @var array setcookie calls that were made
  269. */
  270. public static $_mock_cookies_set = [];
  271. /**
  272. * {@inheritdoc}
  273. */
  274. protected static function _setcookie($name, $value, $expire, $path, $domain, $secure, $httponly)
  275. {
  276. self::$_mock_cookies_set[] = [
  277. 'name' => $name,
  278. 'value' => $value,
  279. 'expire' => $expire,
  280. 'path' => $path,
  281. 'domain' => $domain,
  282. 'secure' => $secure,
  283. 'httponly' => $httponly
  284. ];
  285. return TRUE;
  286. }
  287. /**
  288. * @return int
  289. */
  290. protected static function _time()
  291. {
  292. return KO7_CookieTest::UNIX_TIMESTAMP;
  293. }
  294. }