LoggingStatementTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Database\Log;
  17. use Cake\Core\Configure;
  18. use Cake\Database\DriverInterface;
  19. use Cake\Database\Exception\DatabaseException;
  20. use Cake\Database\Log\LoggingStatement;
  21. use Cake\Database\Log\QueryLogger;
  22. use Cake\Database\StatementInterface;
  23. use Cake\Log\Log;
  24. use Cake\TestSuite\TestCase;
  25. use DateTime;
  26. use TestApp\Error\Exception\MyPDOException;
  27. use TestApp\Error\Exception\MyPDOStringException;
  28. /**
  29. * Tests LoggingStatement class
  30. */
  31. class LoggingStatementTest extends TestCase
  32. {
  33. public function setUp(): void
  34. {
  35. parent::setUp();
  36. Log::setConfig('queries', [
  37. 'className' => 'Array',
  38. 'scopes' => ['queriesLog'],
  39. ]);
  40. }
  41. public function tearDown(): void
  42. {
  43. parent::tearDown();
  44. Log::drop('queries');
  45. Configure::delete('Error.convertStatementToDatabaseException');
  46. }
  47. /**
  48. * Tests that queries are logged when executed without params
  49. */
  50. public function testExecuteNoParams(): void
  51. {
  52. $inner = $this->getMockBuilder(StatementInterface::class)->getMock();
  53. $inner->method('rowCount')->will($this->returnValue(3));
  54. $inner->method('execute')->will($this->returnValue(true));
  55. $driver = $this->getMockBuilder(DriverInterface::class)->getMock();
  56. $st = $this->getMockBuilder(LoggingStatement::class)
  57. ->onlyMethods(['__get'])
  58. ->setConstructorArgs([$inner, $driver])
  59. ->getMock();
  60. $st->expects($this->any())
  61. ->method('__get')
  62. ->willReturn('SELECT bar FROM foo');
  63. $st->setLogger(new QueryLogger(['connection' => 'test']));
  64. $st->execute();
  65. $st->fetchAll();
  66. $messages = Log::engine('queries')->read();
  67. $this->assertCount(1, $messages);
  68. $this->assertMatchesRegularExpression('/^debug: connection=test duration=\d+ rows=3 SELECT bar FROM foo$/', $messages[0]);
  69. }
  70. /**
  71. * Tests that queries are logged when executed with params
  72. */
  73. public function testExecuteWithParams(): void
  74. {
  75. $inner = $this->getMockBuilder(StatementInterface::class)->getMock();
  76. $inner->method('rowCount')->will($this->returnValue(4));
  77. $inner->method('execute')->will($this->returnValue(true));
  78. $driver = $this->getMockBuilder(DriverInterface::class)->getMock();
  79. $st = $this->getMockBuilder(LoggingStatement::class)
  80. ->onlyMethods(['__get'])
  81. ->setConstructorArgs([$inner, $driver])
  82. ->getMock();
  83. $st->expects($this->any())
  84. ->method('__get')
  85. ->willReturn('SELECT bar FROM foo WHERE x=:a AND y=:b');
  86. $st->setLogger(new QueryLogger(['connection' => 'test']));
  87. $st->execute(['a' => 1, 'b' => 2]);
  88. $st->fetchAll();
  89. $messages = Log::engine('queries')->read();
  90. $this->assertCount(1, $messages);
  91. $this->assertMatchesRegularExpression('/^debug: connection=test duration=\d+ rows=4 SELECT bar FROM foo WHERE x=1 AND y=2$/', $messages[0]);
  92. }
  93. /**
  94. * Tests that queries are logged when executed with bound params
  95. */
  96. public function testExecuteWithBinding(): void
  97. {
  98. $inner = $this->getMockBuilder(StatementInterface::class)->getMock();
  99. $inner->method('rowCount')->will($this->returnValue(4));
  100. $inner->method('execute')->will($this->returnValue(true));
  101. $date = new DateTime('2013-01-01');
  102. $inner->expects($this->atLeast(2))
  103. ->method('bindValue')
  104. ->withConsecutive(['a', 1], ['b', $date]);
  105. $driver = $this->getMockBuilder('Cake\Database\Driver')->getMock();
  106. $st = $this->getMockBuilder(LoggingStatement::class)
  107. ->onlyMethods(['__get'])
  108. ->setConstructorArgs([$inner, $driver])
  109. ->getMock();
  110. $st->expects($this->any())
  111. ->method('__get')
  112. ->willReturn('SELECT bar FROM foo WHERE a=:a AND b=:b');
  113. $st->setLogger(new QueryLogger(['connection' => 'test']));
  114. $st->bindValue('a', 1);
  115. $st->bindValue('b', $date, 'date');
  116. $st->execute();
  117. $st->fetchAll();
  118. $st->bindValue('b', new DateTime('2014-01-01'), 'date');
  119. $st->execute();
  120. $st->fetchAll();
  121. $messages = Log::engine('queries')->read();
  122. $this->assertCount(2, $messages);
  123. $this->assertMatchesRegularExpression("/^debug: connection=test duration=\d+ rows=4 SELECT bar FROM foo WHERE a='1' AND b='2013-01-01'$/", $messages[0]);
  124. $this->assertMatchesRegularExpression("/^debug: connection=test duration=\d+ rows=4 SELECT bar FROM foo WHERE a='1' AND b='2014-01-01'$/", $messages[1]);
  125. }
  126. /**
  127. * Tests that queries are logged despite database errors
  128. */
  129. public function testExecuteWithError(): void
  130. {
  131. $this->skipIf(
  132. version_compare(PHP_VERSION, '8.2.0', '>='),
  133. 'Setting queryString on exceptions does not work on 8.2+'
  134. );
  135. $exception = new MyPDOException('This is bad');
  136. $inner = $this->getMockBuilder(StatementInterface::class)->getMock();
  137. $inner->expects($this->once())
  138. ->method('execute')
  139. ->will($this->throwException($exception));
  140. $driver = $this->getMockBuilder(DriverInterface::class)->getMock();
  141. $st = $this->getMockBuilder(LoggingStatement::class)
  142. ->onlyMethods(['__get'])
  143. ->setConstructorArgs([$inner, $driver])
  144. ->getMock();
  145. $st->expects($this->any())
  146. ->method('__get')
  147. ->willReturn('SELECT bar FROM foo');
  148. $st->setLogger(new QueryLogger(['connection' => 'test']));
  149. $this->deprecated(function () use ($st) {
  150. try {
  151. $st->execute();
  152. } catch (MyPDOException $e) {
  153. $this->assertSame('This is bad', $e->getMessage());
  154. $this->assertSame($st->queryString, $e->queryString);
  155. }
  156. });
  157. $messages = Log::engine('queries')->read();
  158. $this->assertCount(1, $messages);
  159. $this->assertMatchesRegularExpression("/^debug: connection=test duration=\d+ rows=0 SELECT bar FROM foo$/", $messages[0]);
  160. }
  161. /**
  162. * Tests that we do exception wrapping correctly.
  163. * The exception returns a string code like most PDOExceptions
  164. */
  165. public function testExecuteWithErrorWrapStatementStringCode(): void
  166. {
  167. Configure::write('Error.convertStatementToDatabaseException', true);
  168. $exception = new MyPDOStringException('This is bad', 1234);
  169. $inner = $this->getMockBuilder(StatementInterface::class)->getMock();
  170. $inner->expects($this->once())
  171. ->method('execute')
  172. ->will($this->throwException($exception));
  173. $driver = $this->getMockBuilder(DriverInterface::class)->getMock();
  174. $st = $this->getMockBuilder(LoggingStatement::class)
  175. ->onlyMethods(['__get'])
  176. ->setConstructorArgs([$inner, $driver])
  177. ->getMock();
  178. $st->expects($this->any())
  179. ->method('__get')
  180. ->willReturn('SELECT bar FROM foo');
  181. $st->setLogger(new QueryLogger(['connection' => 'test']));
  182. try {
  183. $st->execute();
  184. $this->fail('Exception not thrown');
  185. } catch (DatabaseException $e) {
  186. $attrs = $e->getAttributes();
  187. $this->assertSame('This is bad', $e->getMessage());
  188. $this->assertArrayHasKey('queryString', $attrs);
  189. $this->assertSame($st->queryString, $attrs['queryString']);
  190. }
  191. $messages = Log::engine('queries')->read();
  192. $this->assertCount(1, $messages);
  193. $this->assertMatchesRegularExpression("/^debug: connection=test duration=\d+ rows=0 SELECT bar FROM foo$/", $messages[0]);
  194. }
  195. /**
  196. * Tests that we do exception wrapping correctly.
  197. * The exception returns an int code.
  198. */
  199. public function testExecuteWithErrorWrapStatementIntCode(): void
  200. {
  201. Configure::write('Error.convertStatementToDatabaseException', true);
  202. $exception = new MyPDOException('This is bad', 1234);
  203. $inner = $this->getMockBuilder(StatementInterface::class)->getMock();
  204. $inner->expects($this->once())
  205. ->method('execute')
  206. ->will($this->throwException($exception));
  207. $driver = $this->getMockBuilder(DriverInterface::class)->getMock();
  208. $st = $this->getMockBuilder(LoggingStatement::class)
  209. ->onlyMethods(['__get'])
  210. ->setConstructorArgs([$inner, $driver])
  211. ->getMock();
  212. $st->expects($this->any())
  213. ->method('__get')
  214. ->willReturn('SELECT bar FROM foo');
  215. $st->setLogger(new QueryLogger(['connection' => 'test']));
  216. try {
  217. $st->execute();
  218. $this->fail('Exception not thrown');
  219. } catch (DatabaseException $e) {
  220. $attrs = $e->getAttributes();
  221. $this->assertSame('This is bad', $e->getMessage());
  222. $this->assertArrayHasKey('queryString', $attrs);
  223. $this->assertSame($st->queryString, $attrs['queryString']);
  224. }
  225. $messages = Log::engine('queries')->read();
  226. $this->assertCount(1, $messages);
  227. $this->assertMatchesRegularExpression("/^debug: connection=test duration=\d+ rows=0 SELECT bar FROM foo$/", $messages[0]);
  228. }
  229. /**
  230. * Tests setting and getting the logger
  231. */
  232. public function testSetAndGetLogger(): void
  233. {
  234. $logger = new QueryLogger(['connection' => 'test']);
  235. $st = new LoggingStatement(
  236. $this->getMockBuilder(StatementInterface::class)->getMock(),
  237. $this->getMockBuilder(DriverInterface::class)->getMock()
  238. );
  239. $st->setLogger($logger);
  240. $this->assertSame($logger, $st->getLogger());
  241. }
  242. }