Commit bcc9530f authored by DaveD's avatar DaveD
Browse files

Merge branch 'aggregate-test' into 'master'

Add infrastructure for mink tests and some tests for aggregate

See merge request !133
parents d458602c 0c1f52d0
......@@ -260,7 +260,7 @@ class CRM_Cdntaxreceipts_Task_IssueAggregateTaxReceipts extends CRM_Contribute_F
// 4. send the collected PDF for download
// NB: This exits if a file is sent.
cdntaxreceipts_sendCollectedPDF($receiptsForPrintingPDF, 'Receipts-To-Print-' . (int) $_SERVER['REQUEST_TIME'] . '.pdf'); // EXITS.
cdntaxreceipts_sendCollectedPDF($receiptsForPrintingPDF, 'Receipts-To-Print-' . CRM_Cdntaxreceipts_Utils_Time::time() . '.pdf'); // EXITS.
}
}
......@@ -1163,7 +1163,16 @@ function cdntaxreceipts_openCollectedPDF() {
function cdntaxreceipts_sendCollectedPDF(&$pdf, $filename) {
if ( $pdf->getNumPages() > 0 ) {
$pdf->Output($filename, 'D');
if (defined('CIVICRM_TEST')) {
// Put it in uploadDir the same as the other places we put a file, then
// the tests can pick it up and do whatever with it. It would be better
// if there was a way to pass the full $filename to the tests or have
// the tests provide it, but it means refactoring a bit.
$pdf->Output(CRM_Core_Config::singleton()->uploadDir . 'test.pdf', 'F');
}
else {
$pdf->Output($filename, 'D');
}
CRM_Utils_System::civiExit();
}
else {
......
<?php
// @todo fix autoloading in this extension - something is off
require_once 'Base.php';
/**
* @group headless
*/
class CRM_Cdntaxreceipts_AggregateTest extends CRM_Cdntaxreceipts_Base {
public function tearDown(): void {
$this->_tablesToTruncate = [
'cdntaxreceipts_log',
'cdntaxreceipts_log_contributions',
'civicrm_contact',
];
$this->quickCleanUpFinancialEntities();
parent::tearDown();
}
/**
* Test aggregate receipts.
* @dataProvider aggregateProvider
* @param array $input
* @param array $expected
*/
public function testAggregate(array $input, array $expected) {
// set up mock time
$mock_time = '2021-01-02 10:11:12';
\CRM_Cdntaxreceipts_Utils_Time::setTime($mock_time);
$this->setDeliveryMethod(CDNTAX_DELIVERY_PRINT_ONLY);
// create some contributions based on input
$contact[1] = $this->individualCreate([], 1);
$contact[2] = $this->individualCreate([], 2);
$createdContributions = [];
foreach ($input['contributions'] as $contribution) {
// note we just store the id of the result
$createdContributions[] = $this->callAPISuccess('Contribution', 'create', [
'contact_id' => $contact[$contribution['contact_index']],
'financial_type_id' => $contribution['financial_type'],
'total_amount' => $contribution['total_amount'],
'receive_date' => $contribution['receive_date'],
])['id'];
}
// issue some receipts for the totals
$receiptsForPrintingPDF = cdntaxreceipts_openCollectedPDF();
$counter = 0;
foreach ($input['grouped'] as $contact_index => $contributionList) {
// update some things we don't know at dataprovider time
foreach ($contributionList as $loop_index => $c) {
$input['grouped'][$contact_index][$loop_index]['contribution_id'] = $createdContributions[$counter++];
$input['grouped'][$contact_index][$loop_index]['contact_id'] = $contact[$contact_index];
}
// issue receipt for this contact
$status = cdntaxreceipts_issueAggregateTaxReceipt(
$contact[$contact_index],
'2020',
$input['grouped'][$contact_index],
'print',
$receiptsForPrintingPDF,
FALSE
);
$this->assertEquals([
0 => TRUE,
1 => 'print',
2 => NULL,
], $status);
}
// check logs
$records = \CRM_Core_DAO::executeQuery("SELECT * FROM cdntaxreceipts_log")->fetchAll();
// There's other fields we either can't reliably know or don't care about,
// but the expected should match a subset of them.
foreach ($expected as $eid => $expectedContributionLog) {
$realExpected = array(
'issued_on' => (string) strtotime($mock_time),
'contact_id' => (string) $contact[$expectedContributionLog['contact_index']],
'id' => $expectedContributionLog['id'],
'receipt_no' => $expectedContributionLog['receipt_no'],
'receipt_amount' => $expectedContributionLog['receipt_amount'],
'is_duplicate' => $expectedContributionLog['is_duplicate'],
'issue_type' => $expectedContributionLog['issue_type'],
'issue_method' => $expectedContributionLog['issue_method'],
'receipt_status' => $expectedContributionLog['receipt_status'],
);
$intersect = array_intersect_key($records[$eid], $realExpected);
$this->assertNotEmpty($intersect);
$this->assertEquals($realExpected, $intersect);
}
\CRM_Cdntaxreceipts_Utils_Time::reset();
}
/**
* Dataprovider for testAggregate.
* @return array
*/
public function aggregateProvider(): array {
return [
0 => [
'input' => [
'contributions' => [
[
'contact_index' => 1,
'financial_type' => 'Donation',
'total_amount' => '10',
'receive_date' => '2020-01-01',
],
[
'contact_index' => 1,
'financial_type' => 'Donation',
'total_amount' => '20',
'receive_date' => '2020-02-01',
],
[
'contact_index' => 2,
'financial_type' => 'Donation',
'total_amount' => '40',
'receive_date' => '2020-03-01',
],
],
// Index here is the contact index
'grouped' => [
1 => [
[
'total_amount' => '10',
'non_deductible_amount' => 0.0,
'receive_date' => '2020-01-01',
'receive_year' => '2020',
// Note at the time of writing this gets recalculated anyway
// by processTaxReceipt so it doesn't matter what we put here.
'eligible' => TRUE,
'receipt_id' => '0',
],
[
'total_amount' => '20',
'non_deductible_amount' => 0.0,
'receive_date' => '2020-02-01',
'receive_year' => '2020',
'eligible' => TRUE,
'receipt_id' => '0',
],
],
2 => [
[
'total_amount' => '40',
'non_deductible_amount' => 0.0,
'receive_date' => '2020-03-01',
'receive_year' => '2020',
'eligible' => TRUE,
'receipt_id' => '0',
],
],
],
],
'expected' => [
[
'id' => '1',
'contact_index' => 1,
'receipt_no' => 'C-00000001',
'receipt_amount' => '30.00',
'is_duplicate' => '0',
'issue_type' => 'aggregate',
'issue_method' => 'print',
'receipt_status' => 'issued',
],
[
'id' => '2',
'contact_index' => 2,
'receipt_no' => 'C-00000003',
'receipt_amount' => '40.00',
'is_duplicate' => '0',
'issue_type' => 'aggregate',
'issue_method' => 'print',
'receipt_status' => 'issued',
],
],
],
// Same as 0 but one is an Event Fee which should get excluded.
1 => [
'input' => [
'contributions' => [
[
'contact_index' => 1,
'financial_type' => 'Donation',
'total_amount' => '10',
'receive_date' => '2020-01-01',
],
[
'contact_index' => 1,
'financial_type' => 'Event Fee',
'total_amount' => '20',
'receive_date' => '2020-02-01',
],
[
'contact_index' => 2,
'financial_type' => 'Donation',
'total_amount' => '40',
'receive_date' => '2020-03-01',
],
],
// Index here is the contact index
'grouped' => [
1 => [
[
'total_amount' => '10',
'non_deductible_amount' => 0.0,
'receive_date' => '2020-01-01',
'receive_year' => '2020',
'eligible' => TRUE,
'receipt_id' => '0',
],
[
'total_amount' => '20',
'non_deductible_amount' => 0.0,
'receive_date' => '2020-02-01',
'receive_year' => '2020',
// see note above - this gets ignored and recalculated
'eligible' => FALSE,
'receipt_id' => '0',
],
],
2 => [
[
'total_amount' => '40',
'non_deductible_amount' => 0.0,
'receive_date' => '2020-03-01',
'receive_year' => '2020',
'eligible' => TRUE,
'receipt_id' => '0',
],
],
],
],
'expected' => [
[
'id' => '1',
'contact_index' => 1,
'receipt_no' => 'C-00000001',
'receipt_amount' => '10.00',
'is_duplicate' => '0',
'issue_type' => 'aggregate',
'issue_method' => 'print',
'receipt_status' => 'issued',
],
[
'id' => '2',
'contact_index' => 2,
'receipt_no' => 'C-00000003',
'receipt_amount' => '40.00',
'is_duplicate' => '0',
'issue_type' => 'aggregate',
'issue_method' => 'print',
'receipt_status' => 'issued',
],
],
],
];
}
}
<?php
/**
* Base class for tests
*/
class CRM_Cdntaxreceipts_Base extends \CiviUnitTestCase {
public function setUp(): void {
parent::setUp();
// We can't put this in setUpHeadless because then it runs too early and
// our message templates and such get overwritten.
\Civi\Test::headless()->installMe(__DIR__)->apply();
\Civi::settings()->add([
'org_name' => 'CDN Tax Org',
'org_address_line1' => '123 Main St.',
'org_address_line2' => 'Ababa, AA A1A 1A1',
'org_tel' => '123-456-7890',
'org_fax' => '',
'org_email' => 'cdntaxorg@example.org',
'org_web' => 'https://cdntaxorg.example.org',
'org_charitable_no' => '12345-678-RR0001',
'receipt_prefix' => 'C-',
'receipt_serial' => 0,
'receipt_authorized_signature_text' => 'Receet Sighnor',
'issue_inkind' => 0,
'delivery_method' => CDNTAX_DELIVERY_DATA_ONLY,
'attach_to_workflows' => 0,
'enable_advanced_eligibility_report' => 0,
'email_from' => 'cdntaxorg@example.org',
'email_archive' => 'cdntaxorg@example.org',
]);
}
/**
* Set a delivery method.
* The default is data only, which is useful for just testing the logs.
* If we want a different method, we need to also remember to adjust the
* mail settings since otherwise drupal gives errors about failed mail.
* @param int $method
*/
protected function setDeliveryMethod(int $method) {
$mb = \Civi::settings()->get('mailing_backend');
\Civi::settings()->set('mailing_backend', array_merge($mb, ['outBound_option' => \CRM_Mailing_Config::OUTBOUND_OPTION_MOCK]));
\Civi::settings()->set('delivery_method', $method);
}
}
<?php
namespace Cdntaxreceipts\Tests\Mink;
require_once 'CdntaxreceiptsBase.php';
class AggregateTest extends CdntaxreceiptsBase {
/**
* @var array
* We always create one contact to start with.
*/
protected $contact;
public function setUp(): void {
parent::setUp();
$this->createUserAndLogIn();
$this->contact = $this->createContact();
}
public function testAggregateTaxReceipt() {
// set up mock time
$mock_time = '2021-01-02 10:11:12';
\CRM_Cdntaxreceipts_Utils_Time::setTime($mock_time);
$this->setDeliveryMethod(CDNTAX_DELIVERY_PRINT_ONLY);
$contribution1_1 = civicrm_api3('Contribution', 'create', [
'contact_id' => $this->contact['id'],
'financial_type_id' => 'Donation',
'total_amount' => '10',
'receive_date' => '2020-01-01',
]);
$contribution1_2 = civicrm_api3('Contribution', 'create', [
'contact_id' => $this->contact['id'],
'financial_type_id' => 'Donation',
'total_amount' => '20',
'receive_date' => '2020-02-01',
]);
$contact2 = $this->createContact(1);
$contribution2_1 = civicrm_api3('Contribution', 'create', [
'contact_id' => $contact2['id'],
'financial_type_id' => 'Donation',
'total_amount' => '40',
'receive_date' => '2020-03-01',
]);
// search
$this->drupalGet(\CRM_Utils_System::url("civicrm/contribute/search", "reset=1&force=1", TRUE, NULL, FALSE));
$this->assertPageHasNoErrorMessages();
// choose all
$this->getSession()->getPage()->selectFieldOption('radio_ts', 'ts_all');
// select aggregate task
$this->getSession()->executeScript("CRM.$('#task').val('406').trigger('change');");
$this->assertPageHasNoErrorMessages();
// Pick the receipt year and issue receipt
$this->getSession()->getPage()->selectFieldOption('receipt_year', "issue_2020");
$this->getSession()->getPage()->pressButton('_qf_IssueAggregateTaxReceipts_next');
$this->assertPageHasNoErrorMessages();
// check logs
$records = \CRM_Core_DAO::executeQuery("SELECT * FROM cdntaxreceipts_log")->fetchAll();
$expecteds = array(
0 => array(
'id' => '1',
'issued_on' => (string) strtotime($mock_time),
'receipt_no' => 'C-00000003',
'contact_id' => (string) $contact2['id'],
'receipt_amount' => '40.00',
'is_duplicate' => '0',
'issue_type' => 'aggregate',
'issue_method' => 'print',
'receipt_status' => 'issued',
),
1 => array(
'id' => '2',
'issued_on' => (string) strtotime($mock_time),
'receipt_no' => 'C-00000002',
'contact_id' => (string) $this->contact['id'],
'receipt_amount' => '30.00',
'is_duplicate' => '0',
'issue_type' => 'aggregate',
'issue_method' => 'print',
'receipt_status' => 'issued',
),
);
// There's other fields we either can't reliably know or don't care about,
// but the expected should match a subset of them.
foreach ($expecteds as $eid => $expected) {
$intersect = array_intersect_key($records[$eid], $expected);
$this->assertNotEmpty($intersect);
$this->assertEquals($expected, $intersect);
}
$this->assertExpectedPDF(__CLASS__, __FUNCTION__);
\CRM_Cdntaxreceipts_Utils_Time::reset();
$this->htmlOutput();
}
}
......@@ -49,36 +49,6 @@ class CdntaxreceiptsBase extends CiviCrmTestBase {
$this->_loggedInUser = $account->uid;
}
/**
* Create a contact. Helper function since the usual individualCreate()
* is not available.
* @param int $index There are a couple stock contacts. You can pick one as the base params to use.
* @param array $params Some params to merge into the base.
* @return array
*/
public function createContact(int $index = 0, array $params = []): array {
$stockContacts = [
'first_name' => ['Anthony', 'Joe', 'Terrence', 'Lucie', 'Albert', 'Bill', 'Kim'],
'last_name' => ['Anderson', 'Miller', 'Smith', 'Collins', 'Peterson', 'Johnson', 'Li'],
];
$vars = array_merge([
'contact_type' => 'Individual',
'first_name' => $stockContacts['first_name'][$index],
'last_name' => $stockContacts['last_name'][$index],
'email' => strtolower("{$stockContacts['first_name'][$index]}.{$stockContacts['last_name'][$index]}@example.org"),
'phone' => preg_replace('/[^0-9]/', '', bin2hex("{$stockContacts['first_name'][$index]}{$stockContacts['last_name'][$index]}")),
], $params);
// Phone doesn't work the same as email for create.
// If the input params didn't blank it out, convert to right format.
if (!empty($vars['phone'])) {
$vars['api.Phone.create'] = ['phone' => $vars['phone']];
unset($vars['phone']);
}
return civicrm_api3('Contact', 'create', $vars);
}
/**
* Configure the extension, i.e. fill out the settings page.
*/
......@@ -104,4 +74,93 @@ class CdntaxreceiptsBase extends CiviCrmTestBase {
]);
}
/**
* Set a delivery method.
* The default is data only, which is useful for just testing the logs.
* If we want a different method, we need to also remember to adjust the
* mail settings since otherwise drupal gives errors about failed mail.
* @param int $method
*/
protected function setDeliveryMethod(int $method) {
$mb = \Civi::settings()->get('mailing_backend');
\Civi::settings()->set('mailing_backend', array_merge($mb, ['outBound_option' => \CRM_Mailing_Config::OUTBOUND_OPTION_MOCK]));
\Civi::settings()->set('delivery_method', $method);
}
/**
* Compare two files efficiently.
* @param string $a First file
* @param string $b Second file
* @return bool
*/
private function compareFiles($a, $b): bool {
// Check if filesize is different
if (filesize($a) !== filesize($b)) {
return FALSE;
}
// Check if content is different
$ah = fopen($a, 'rb');
$bh = fopen($b, 'rb');
$result = TRUE;
while (!feof($ah)) {
if (fread($ah, 8192) != fread($bh, 8192)) {
$result = FALSE;
break;
}
}
fclose($ah);
fclose($bh);
return $result;
}
/**
* Return the full path to the associated fixture file for a given function.
* @param string $class The fully qualified class name.
* @param string $func The function name. Who called us.
* @param string $type The file type.
* @return string
*/
protected function getFixtureFileFor(string $class, string $func, $type = '.pdf') {
return str_replace('\\', '/', __DIR__) . '/fixtures/' . preg_replace('/[^a-zA-Z0-9_]/', '_', "{$class}{$func}") . $type;
}
/**
* There should be a file named test.pdf created when running under tests
* that generate pdfs. We have to fudge the creation dates inside it a bit
* but otherwise it should be identical to one we're expecting.
* @param string $class The fully qualified class name.
* @param string $func The function name. Who called us.
*/
protected function assertExpectedPDF(string $class, string $func) {
// Replace windows backslashes.
$pdf_file = str_replace('\\', '/', \CRM_Core_Config::singleton()->uploadDir . 'test.pdf');
$this->assertTrue(file_exists($pdf_file));
$new_name = str_replace('\\', '/', $this->getBrowserOutputDirectory()) . 'Receipts-To-Print-' . \CRM_Cdntaxreceipts_Utils_Time::time() . '.pdf';
$this->assertTrue(rename($pdf_file, $new_name), "Can't rename $pdf_file to $new_name");
$this->fudgePDFFile($new_name);
$expectedFile = $this->getFixtureFileFor($class, $func);
$this->assertTrue($this->compareFiles($new_name, $expectedFile), "$new_name differs from $expectedFile");
}
/**
* Make the dates in the file match our mock timestamp.
* Also there's a uuid, which we change to match our fixtures.
* @param string $filename
*/
protected function fudgePDFFile(string $filename) {
$s = file_get_contents($filename);
// The +10 is because for some reason that's the timezone that ends up
// in our fixture file.
$s = preg_replace('/\d{14}\+\d\d/', date('YmdHis', \CRM_Cdntaxreceipts_Utils_Time::time()) . '+10', $s);
$s = preg_replace('/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\+\d\d/', date('Y-m-d\TH:i:s', \CRM_Cdntaxreceipts_Utils_Time::time()) . '+10', $s);
$s = preg_replace('/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/', '9e7bde6b-2ad3-c6ba-6656-86ba3cf7b7a2', $s);
$s = preg_replace('/<[a-f0-9]{32}>/', '<9e7bde6b2ad3c6ba665686ba3cf7b7a2>', $s);
$this->assertGreaterThan(0, file_put_contents($filename, $s));
}
}
......@@ -48,4 +48,25 @@ class IssueTest extends CdntaxreceiptsBase {
$this->htmlOutput();
}
/**
* This is identical to testIssueTaxReceipt() but we use print method.
* We don't verify the PDF here.
*/
public function testIssueTaxReceiptPrint() {
$this->setDeliveryMethod(CDNTAX_DELIVERY_PRINT_ONLY);
$this->testIssueTaxReceipt();
$this->assertSession()->pageTextContains('Please download and print the receipt that is generated. You will need to send a printed copy to the contributor.');
}
/**
* This is identical to testIssueTaxReceipt() but we use email method.
* We don't verify the PDF here.
*/
public function testIssueTaxReceiptEmail() {
$this->setDeliveryMethod(CDNTAX_DELIVERY_PRINT_EMAIL);