Merge pull request #7797 from JKingsnorth/CRM-17977
[civicrm-core.git] / tests / phpunit / api / v3 / LoggingTest.php
CommitLineData
1f43f9e2 1<?php
2/*
3 +--------------------------------------------------------------------+
4| CiviCRM version 4.7 |
5+--------------------------------------------------------------------+
6| Copyright CiviCRM LLC (c) 2004-2015 |
7+--------------------------------------------------------------------+
8| This file is a part of CiviCRM. |
9| |
10| CiviCRM is free software; you can copy, modify, and distribute it |
11| under the terms of the GNU Affero General Public License |
12| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13| |
14| CiviCRM is distributed in the hope that it will be useful, but |
15| WITHOUT ANY WARRANTY; without even the implied warranty of |
16| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17| See the GNU Affero General Public License for more details. |
18| |
19| You should have received a copy of the GNU Affero General Public |
20| License and the CiviCRM Licensing Exception along |
21| with this program; if not, contact CiviCRM LLC |
22| at info[AT]civicrm[DOT]org. If you have questions about the |
23| GNU Affero General Public License or the licensing of CiviCRM, |
24| see the CiviCRM license FAQ at http://civicrm.org/licensing |
25+--------------------------------------------------------------------+
26 */
27
28/**
29 * Test class for Logging API.
30 *
31 * @package CiviCRM
32 * @group headless
33 */
34class api_v3_LoggingTest extends CiviUnitTestCase {
35
36 /**
37 * Sets up the fixture, for example, opens a network connection.
38 *
39 * This method is called before a test is executed.
40 */
41 protected function setUp() {
87a52027 42 $this->ensureTempColIsCleanedUp();
1f43f9e2 43 parent::setUp();
44 }
45
46 /**
47 * Clean up log tables.
48 */
49 protected function tearDown() {
29444295 50 $this->quickCleanup(array('civicrm_email'));
1f43f9e2 51 parent::tearDown();
52 $this->callAPISuccess('Setting', 'create', array('logging' => FALSE));
53 $schema = new CRM_Logging_Schema();
54 $schema->dropAllLogTables();
55 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_setting WHERE name LIKE 'logg%'");
56 }
57
58 /**
59 * Test that logging is successfully enabled and disabled.
60 */
61 public function testEnableDisableLogging() {
87a52027 62 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging')));
1f43f9e2 63 $this->assertLoggingEnabled(FALSE);
64
65 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
66 $this->assertLoggingEnabled(TRUE);
67 $this->checkLogTableCreated();
d7ea7150 68 $this->checkTriggersCreated(TRUE);
1f43f9e2 69 // Create a contact to make sure they aren't borked.
70 $this->individualCreate();
87a52027 71 $this->assertTrue($this->callAPISuccessGetValue('Setting', array('name' => 'logging')));
72 $this->assertEquals(1, $this->callAPISuccessGetValue('Setting', array('name' => 'logging_all_tables_uniquid')));
d7ea7150 73 $this->assertEquals(
74 date('Y-m-d'),
87a52027 75 date('Y-m-d', strtotime($this->callAPISuccessGetValue('Setting', array('name' => 'logging_uniqueid_date'))))
d7ea7150 76 );
1f43f9e2 77
78 $this->callAPISuccess('Setting', 'create', array('logging' => FALSE));
87a52027 79 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging')));
1f43f9e2 80 $this->assertLoggingEnabled(FALSE);
81 }
82
83 /**
84 * Test that logging is successfully enabled and disabled.
85 */
86 public function testEnableDisableLoggingWithTriggerHook() {
87 $this->hookClass->setHook('civicrm_alterLogTables', array($this, 'innodbLogTableSpec'));
88 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
89 $this->checkINNODBLogTableCreated();
d7ea7150 90 $this->checkTriggersCreated(TRUE);
1f43f9e2 91 // Create a contact to make sure they aren't borked.
92 $this->individualCreate();
93 $this->callAPISuccess('Setting', 'create', array('logging' => FALSE));
94 }
95
d7ea7150 96 /**
97 * Check responsible creation when old structure log table exists.
98 *
99 * When an existing table exists NEW tables will have the varchar type for log_conn_id.
100 *
101 * Existing tables will be unchanged, and the trigger will use log_conn_id
102 * rather than uniqueId to be consistent across the tables.
103 *
104 * The settings for unique id will not be set.
105 */
106 public function testEnableLoggingLegacyLogTableExists() {
107 $this->createLegacyStyleContactLogTable();
108 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
109 $this->checkTriggersCreated(FALSE);
87a52027 110 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging_all_tables_uniquid')));
111 $this->assertEmpty($this->callAPISuccessGetValue('Setting', array('name' => 'logging_uniqueid_date')));
d7ea7150 112 }
113
114 /**
115 * Check we can update legacy log tables using the api function.
116 */
117 public function testUpdateLegacyLogTable() {
118 $this->createLegacyStyleContactLogTable();
119 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
120 $this->callAPISuccess('System', 'updatelogtables', array());
121 $this->checkLogTableCreated();
122 $this->checkTriggersCreated(TRUE);
87a52027 123 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging_all_tables_uniquid')));
d7ea7150 124 $this->assertEquals(
125 date('Y-m-d'),
87a52027 126 date('Y-m-d', strtotime($this->callAPISuccessGetValue('Setting', array('name' => 'logging_uniqueid_date'))))
d7ea7150 127 );
128 }
129
130 /**
131 * Check we can update legacy log tables using the api function.
132 */
133 public function testUpdateLogTableHookINNODB() {
134 $this->createLegacyStyleContactLogTable();
135 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
136 $this->hookClass->setHook('civicrm_alterLogTables', array($this, 'innodbLogTableSpec'));
137 $this->callAPISuccess('System', 'updatelogtables', array());
138 $this->checkINNODBLogTableCreated();
139 $this->checkTriggersCreated(TRUE);
140 // Make sure that the absence of a hook specifying INNODB does not cause revert to archive.
141 // Only a positive action, like specifying ARCHIVE in a hook should trigger a change back to archive.
142 $this->hookClass->setHook('civicrm_alterLogTables', array());
143 $schema = new CRM_Logging_Schema();
144 $spec = $schema->getLogTableSpec();
145 $this->assertEquals(array(), $spec['civicrm_contact']);
146 $this->callAPISuccess('System', 'updatelogtables', array());
147 $this->checkINNODBLogTableCreated();
148 }
149
003b4269 150 /**
151 * Check that if a field is added then the trigger is updated on refresh.
152 */
153 public function testRebuildTriggerAfterSchemaChange() {
154 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
155 $tables = array('civicrm_acl', 'civicrm_website');
156 foreach ($tables as $table) {
157 CRM_Core_DAO::executeQuery("ALTER TABLE $table ADD column temp_col INT(10)");
158 }
159
160 $schema = new CRM_Logging_Schema();
161 $schema->fixSchemaDifferencesForAll(TRUE);
162
163 foreach ($tables as $table) {
87a52027 164 $this->assertTrue($this->checkColumnExistsInTable('log_' . $table, 'temp_col'), 'log_' . $table . ' has temp_col');
003b4269 165 $dao = CRM_Core_DAO::executeQuery("SHOW TRIGGERS LIKE '{$table}'");
166 while ($dao->fetch()) {
167 $this->assertContains('temp_col', $dao->Statement);
168 }
169 }
170 CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_acl DROP column temp_col");
171 CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_website DROP column temp_col");
172 }
173
1f43f9e2 174 /**
175 * Use a hook to declare an INNODB engine for the contact log table.
176 *
177 * @param array $logTableSpec
178 */
179 public function innodbLogTableSpec(&$logTableSpec) {
180 $logTableSpec['civicrm_contact'] = array(
d7ea7150 181 'engine' => 'InnoDB',
182 'engine_config' => 'ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4',
1f43f9e2 183 'indexes' => array(
184 'index_id' => 'id',
185 'index_log_conn_id' => 'log_conn_id',
186 'index_log_date' => 'log_date',
187 ),
188 );
189 }
190
191 /**
192 * Check the log tables were created and look OK.
193 */
194 protected function checkLogTableCreated() {
195 $dao = CRM_Core_DAO::executeQuery("SHOW CREATE TABLE log_civicrm_contact");
196 $dao->fetch();
197 $this->assertEquals('log_civicrm_contact', $dao->Table);
198 $tableField = 'Create_Table';
199 $this->assertContains('`log_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,', $dao->$tableField);
d7ea7150 200 $this->assertContains('`log_conn_id` varchar(17)', $dao->$tableField);
1f43f9e2 201 return $dao->$tableField;
202 }
203
204 /**
205 * Check the log tables were created and reflect the INNODB hook.
206 */
207 protected function checkINNODBLogTableCreated() {
208 $createTableString = $this->checkLogTableCreated();
209 $this->assertContains('ENGINE=InnoDB', $createTableString);
d7ea7150 210 $this->assertContains('ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4', $createTableString);
1f43f9e2 211 $this->assertContains('KEY `index_id` (`id`),', $createTableString);
212 }
213
214 /**
215 * Check the triggers were created and look OK.
216 */
d7ea7150 217 protected function checkTriggersCreated($unique) {
1f43f9e2 218 $dao = CRM_Core_DAO::executeQuery("SHOW TRIGGERS LIKE 'civicrm_contact'");
219 while ($dao->fetch()) {
220 if ($dao->Timing == 'After') {
d7ea7150 221 if ($unique) {
222 $this->assertContains('@uniqueID', $dao->Statement);
223 }
224 else {
225 $this->assertContains('CONNECTION_ID()', $dao->Statement);
226 }
1f43f9e2 227 }
228 }
229 }
230
231 /**
232 * Assert logging is enabled or disabled as per input parameter.
233 *
234 * @param bool $expected
235 * Do we expect it to be enabled.
236 */
237 protected function assertLoggingEnabled($expected) {
238 $schema = new CRM_Logging_Schema();
239 $this->assertTrue($schema->isEnabled() === $expected);
240 }
241
d7ea7150 242 /**
243 * Create the contact log table with log_conn_id as an integer.
244 */
245 protected function createLegacyStyleContactLogTable() {
246 CRM_Core_DAO::executeQuery("
247 CREATE TABLE log_civicrm_contact
248 (log_conn_id INT NULL, log_user_id INT NULL, log_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP)
249 ENGINE=ARCHIVE
250 (SELECT c.*, CURRENT_TIMESTAMP as log_date, 'Initialize' as 'log_action'
251 FROM civicrm_contact c)
252 ");
253 }
254
93afbc3a 255 /**
256 * Test changes can be reverted.
257 */
258 public function testRevert() {
259 $contactId = $this->individualCreate();
260 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
261 CRM_Core_DAO::executeQuery("SET @uniqueID = 'woot'");
262 $timeStamp = date('Y-m-d H:i:s');
263 $this->callAPISuccess('Contact', 'create', array(
264 'id' => $contactId,
265 'first_name' => 'Dopey',
266 'api.email.create' => array('email' => 'dopey@mail.com'))
267 );
268 $email = $this->callAPISuccessGetSingle('email', array('email' => 'dopey@mail.com'));
269 $this->callAPIAndDocument('Logging', 'revert', array('log_conn_id' => 'woot', 'log_date' => $timeStamp), __FILE__, 'Revert');
270 $this->assertEquals('Anthony', $this->callAPISuccessGetValue('contact', array('id' => $contactId, 'return' => 'first_name')));
271 $this->callAPISuccessGetCount('Email', array('id' => $email['id']), 0);
272 }
273
99008b08 274 /**
275 * Test changes can be reverted.
276 */
277 public function testRevertNoDate() {
278 $contactId = $this->individualCreate();
279 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
280 CRM_Core_DAO::executeQuery("SET @uniqueID = 'Wot woot'");
281 $this->callAPISuccess('Contact', 'create', array(
282 'id' => $contactId,
283 'first_name' => 'Dopey',
284 'api.email.create' => array('email' => 'dopey@mail.com'))
285 );
286 $email = $this->callAPISuccessGetSingle('email', array('email' => 'dopey@mail.com'));
287 $this->callAPISuccess('Logging', 'revert', array('log_conn_id' => 'Wot woot'));
288 $this->assertEquals('Anthony', $this->callAPISuccessGetValue('contact', array('id' => $contactId, 'return' => 'first_name')));
289 $this->callAPISuccessGetCount('Email', array('id' => $email['id']), 0);
290 }
291
292 /**
293 * Test changes can be reverted.
294 */
295 public function testRevertNoDateNotUnique() {
296 $contactId = $this->individualCreate();
297 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
298 CRM_Core_DAO::executeQuery("SET @uniqueID = 'Wopity woot'");
299 $this->callAPISuccess('Contact', 'create', array(
300 'id' => $contactId,
301 'first_name' => 'Dopey',
302 'api.email.create' => array('email' => 'dopey@mail.com'))
303 );
304 $this->callAPISuccess('Setting', 'create', array('logging_all_tables_uniquid' => FALSE));
a8cd67b5 305 $this->callAPISuccess('Setting', 'create', array('logging_uniqueid_date' => date('Y-m-d H:i:s', strtotime('+ 1 hour'))));
99008b08 306 $this->callAPIFailure(
307 'Logging',
308 'revert',
309 array('log_conn_id' => 'Wopity woot'),
310 'Failure in api call for Logging revert: The connection date must be passed in to disambiguate this logging entry per CRM-18193'
311 );
312 }
313
29444295 314 /**
315 * Test changes can be retrieved.
316 */
317 public function testGet() {
318 $contactId = $this->individualCreate();
319 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
320 CRM_Core_DAO::executeQuery("SET @uniqueID = 'wooty woot'");
321 $timeStamp = date('Y-m-d H:i:s');
322 $this->callAPISuccess('Contact', 'create', array(
323 'id' => $contactId,
324 'first_name' => 'Dopey',
325 'last_name' => 'Dwarf',
326 'api.email.create' => array('email' => 'dopey@mail.com'))
327 );
328 $this->callAPISuccessGetSingle('email', array('email' => 'dopey@mail.com'));
99008b08 329 $diffs = $this->callAPISuccess('Logging', 'get', array('log_conn_id' => 'wooty woot', 'log_date' => $timeStamp), __FUNCTION__, __FILE__);
330 $this->assertLoggingIncludes($diffs['values'], array('to' => 'Dwarf, Dopey'));
331 $this->assertLoggingIncludes($diffs['values'], array('to' => 'Mr. Dopey Dwarf II', 'table' => 'civicrm_contact', 'action' => 'Update', 'field' => 'display_name'));
332 $this->assertLoggingIncludes($diffs['values'], array('to' => 'dopey@mail.com', 'table' => 'civicrm_email', 'action' => 'Insert', 'field' => 'email'));
333 }
334
335 /**
336 * Test changes can be retrieved without log_date being required.
337 */
338 public function testGetNoDate() {
339 $contactId = $this->individualCreate();
340 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
341 CRM_Core_DAO::executeQuery("SET @uniqueID = 'wooty wop wop'");
342 $this->callAPISuccess('Contact', 'create', array(
343 'id' => $contactId,
344 'first_name' => 'Dopey',
345 'last_name' => 'Dwarf',
346 'api.email.create' => array('email' => 'dopey@mail.com'))
347 );
348 $this->callAPISuccessGetSingle('email', array('email' => 'dopey@mail.com'));
349 $diffs = $this->callAPIAndDocument('Logging', 'get', array('log_conn_id' => 'wooty wop wop'), __FUNCTION__, __FILE__);
29444295 350 $this->assertLoggingIncludes($diffs['values'], array('to' => 'Dwarf, Dopey'));
351 $this->assertLoggingIncludes($diffs['values'], array('to' => 'Mr. Dopey Dwarf II', 'table' => 'civicrm_contact', 'action' => 'Update', 'field' => 'display_name'));
352 $this->assertLoggingIncludes($diffs['values'], array('to' => 'dopey@mail.com', 'table' => 'civicrm_email', 'action' => 'Insert', 'field' => 'email'));
353 }
354
355 /**
356 * Assert the values in the $expect array in included in the logging diff.
357 *
358 * @param array $diffs
359 * @param array $expect
360 *
361 * @return bool
362 * @throws \CRM_Core_Exception
363 */
364 public function assertLoggingIncludes($diffs, $expect) {
365 foreach ($diffs as $diff) {
366 foreach ($expect as $expectKey => $expectValue) {
367 if ($diff[$expectKey] != $expectValue) {
368 continue;
369 }
370 return TRUE;
371 }
372 }
373 throw new CRM_Core_Exception("No match found for key : $expectKey with value : $expectValue");
374 }
375
87a52027 376 /**
377 * Check if the column exists in the table.
378 *
379 * @param string $table
380 * @param string $column
381 *
382 * @return \CRM_Core_DAO|object
383 */
384 protected function checkColumnExistsInTable($table, $column) {
385 $dao = CRM_Core_DAO::executeQuery("SHOW columns FROM {$table} WHERE Field = '{$column}'");
386 $dao->fetch(TRUE);
387 return ($dao->N == 1);
388 }
389
390 /**
391 * Helper for when it crashes and clean up needs to be done.
392 */
393 protected function ensureTempColIsCleanedUp() {
394 if ($this->checkColumnExistsInTable('civicrm_acl', 'temp_col')) {
395 CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_acl DROP Column temp_col");
396 CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_website DROP Column temp_col");
397 }
398 }
399
1f43f9e2 400}