3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
29 * Test class for Logging API.
34 class api_v3_LoggingTest
extends CiviUnitTestCase
{
37 * Sets up the fixture, for example, opens a network connection.
39 * This method is called before a test is executed.
41 protected function setUp() {
42 $this->ensureTempColIsCleanedUp();
47 * Clean up log tables.
49 protected function tearDown() {
50 $this->quickCleanup(array('civicrm_email'));
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%'");
59 * Test that logging is successfully enabled and disabled.
61 public function testEnableDisableLogging() {
62 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging')));
63 $this->assertLoggingEnabled(FALSE);
65 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
66 $this->assertLoggingEnabled(TRUE);
67 $this->checkLogTableCreated();
68 $this->checkTriggersCreated(TRUE);
69 // Create a contact to make sure they aren't borked.
70 $this->individualCreate();
71 $this->assertTrue($this->callAPISuccessGetValue('Setting', array('name' => 'logging')));
72 $this->assertEquals(1, $this->callAPISuccessGetValue('Setting', array('name' => 'logging_all_tables_uniquid')));
75 date('Y-m-d', strtotime($this->callAPISuccessGetValue('Setting', array('name' => 'logging_uniqueid_date'))))
78 $this->callAPISuccess('Setting', 'create', array('logging' => FALSE));
79 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging')));
80 $this->assertLoggingEnabled(FALSE);
84 * Test that logging is successfully enabled and disabled.
86 public function testEnableDisableLoggingWithTriggerHook() {
87 $this->hookClass
->setHook('civicrm_alterLogTables', array($this, 'innodbLogTableSpec'));
88 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
89 $this->checkINNODBLogTableCreated();
90 $this->checkTriggersCreated(TRUE);
91 // Create a contact to make sure they aren't borked.
92 $this->individualCreate();
93 $this->callAPISuccess('Setting', 'create', array('logging' => FALSE));
97 * Check responsible creation when old structure log table exists.
99 * When an existing table exists NEW tables will have the varchar type for log_conn_id.
101 * Existing tables will be unchanged, and the trigger will use log_conn_id
102 * rather than uniqueId to be consistent across the tables.
104 * The settings for unique id will not be set.
106 public function testEnableLoggingLegacyLogTableExists() {
107 $this->createLegacyStyleContactLogTable();
108 $this->callAPISuccess('Setting', 'create', array('logging' => TRUE));
109 $this->checkTriggersCreated(FALSE);
110 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging_all_tables_uniquid')));
111 $this->assertEmpty($this->callAPISuccessGetValue('Setting', array('name' => 'logging_uniqueid_date')));
115 * Check we can update legacy log tables using the api function.
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);
123 $this->assertEquals(0, $this->callAPISuccessGetValue('Setting', array('name' => 'logging_all_tables_uniquid')));
126 date('Y-m-d', strtotime($this->callAPISuccessGetValue('Setting', array('name' => 'logging_uniqueid_date'))))
131 * Check we can update legacy log tables using the api function.
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();
151 * Check that if a field is added then the trigger is updated on refresh.
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)");
160 $schema = new CRM_Logging_Schema();
161 $schema->fixSchemaDifferencesForAll(TRUE);
163 foreach ($tables as $table) {
164 $this->assertTrue($this->checkColumnExistsInTable('log_' . $table, 'temp_col'), 'log_' . $table . ' has temp_col');
165 $dao = CRM_Core_DAO
::executeQuery("SHOW TRIGGERS LIKE '{$table}'");
166 while ($dao->fetch()) {
167 $this->assertContains('temp_col', $dao->Statement
);
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");
175 * Use a hook to declare an INNODB engine for the contact log table.
177 * @param array $logTableSpec
179 public function innodbLogTableSpec(&$logTableSpec) {
180 $logTableSpec['civicrm_contact'] = array(
181 'engine' => 'InnoDB',
182 'engine_config' => 'ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4',
185 'index_log_conn_id' => 'log_conn_id',
186 'index_log_date' => 'log_date',
192 * Check the log tables were created and look OK.
194 protected function checkLogTableCreated() {
195 $dao = CRM_Core_DAO
::executeQuery("SHOW CREATE TABLE log_civicrm_contact");
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);
200 $this->assertContains('`log_conn_id` varchar(17)', $dao->$tableField);
201 return $dao->$tableField;
205 * Check the log tables were created and reflect the INNODB hook.
207 protected function checkINNODBLogTableCreated() {
208 $createTableString = $this->checkLogTableCreated();
209 $this->assertContains('ENGINE=InnoDB', $createTableString);
210 $this->assertContains('ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4', $createTableString);
211 $this->assertContains('KEY `index_id` (`id`),', $createTableString);
215 * Check the triggers were created and look OK.
217 protected function checkTriggersCreated($unique) {
218 $dao = CRM_Core_DAO
::executeQuery("SHOW TRIGGERS LIKE 'civicrm_contact'");
219 while ($dao->fetch()) {
220 if ($dao->Timing
== 'After') {
222 $this->assertContains('@uniqueID', $dao->Statement
);
225 $this->assertContains('CONNECTION_ID()', $dao->Statement
);
232 * Assert logging is enabled or disabled as per input parameter.
234 * @param bool $expected
235 * Do we expect it to be enabled.
237 protected function assertLoggingEnabled($expected) {
238 $schema = new CRM_Logging_Schema();
239 $this->assertTrue($schema->isEnabled() === $expected);
243 * Create the contact log table with log_conn_id as an integer.
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)
250 (SELECT c.*, CURRENT_TIMESTAMP as log_date, 'Initialize' as 'log_action'
251 FROM civicrm_contact c)
256 * Test changes can be reverted.
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(
265 'first_name' => 'Dopey',
266 'api.email.create' => array('email' => 'dopey@mail.com'))
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);
275 * Test changes can be reverted.
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(
283 'first_name' => 'Dopey',
284 'api.email.create' => array('email' => 'dopey@mail.com'))
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);
293 * Test changes can be reverted.
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(
301 'first_name' => 'Dopey',
302 'api.email.create' => array('email' => 'dopey@mail.com'))
304 $this->callAPISuccess('Setting', 'create', array('logging_all_tables_uniquid' => FALSE));
305 $this->callAPISuccess('Setting', 'create', array('logging_uniqueid_date' => date('Y-m-d H:i:s', strtotime('+ 1 hour'))));
306 $this->callAPIFailure(
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'
315 * Test changes can be retrieved.
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(
324 'first_name' => 'Dopey',
325 'last_name' => 'Dwarf',
326 'api.email.create' => array('email' => 'dopey@mail.com'))
328 $this->callAPISuccessGetSingle('email', array('email' => 'dopey@mail.com'));
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'));
336 * Test changes can be retrieved without log_date being required.
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(
344 'first_name' => 'Dopey',
345 'last_name' => 'Dwarf',
346 'api.email.create' => array('email' => 'dopey@mail.com'))
348 $this->callAPISuccessGetSingle('email', array('email' => 'dopey@mail.com'));
349 $diffs = $this->callAPIAndDocument('Logging', 'get', array('log_conn_id' => 'wooty wop wop'), __FUNCTION__
, __FILE__
);
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'));
356 * Assert the values in the $expect array in included in the logging diff.
358 * @param array $diffs
359 * @param array $expect
362 * @throws \CRM_Core_Exception
364 public function assertLoggingIncludes($diffs, $expect) {
365 foreach ($diffs as $diff) {
366 foreach ($expect as $expectKey => $expectValue) {
367 if ($diff[$expectKey] != $expectValue) {
373 throw new CRM_Core_Exception("No match found for key : $expectKey with value : $expectValue");
377 * Check if the column exists in the table.
379 * @param string $table
380 * @param string $column
382 * @return \CRM_Core_DAO|object
384 protected function checkColumnExistsInTable($table, $column) {
385 $dao = CRM_Core_DAO
::executeQuery("SHOW columns FROM {$table} WHERE Field = '{$column}'");
387 return ($dao->N
== 1);
391 * Helper for when it crashes and clean up needs to be done.
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");