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 | */ |
34 | class 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 | } |