--- /dev/null
+<?php
+
+use CRM_Financialacls_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+use Civi\Api4\PriceField;
+
+/**
+ * FIXME - Add test description.
+ *
+ * Tips:
+ * - With HookInterface, you may implement CiviCRM hooks directly in the test class.
+ * Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar).
+ * - With TransactionalInterface, any data changes made by setUp() or test****() functions will
+ * rollback automatically -- as long as you don't manipulate schema or truncate tables.
+ * If this test needs to manipulate schema or truncate tables, then either:
+ * a. Do all that using setupHeadless() and Civi\Test.
+ * b. Disable TransactionalInterface, and handle all setup/teardown yourself.
+ *
+ * @group headless
+ */
+class LineItemTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ use Civi\Test\ContactTestTrait;
+ use Civi\Test\Api3TestTrait;
+
+ /**
+ * @return \Civi\Test\CiviEnvBuilder
+ * @throws \CRM_Extension_Exception_ParseException
+ */
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()
+ ->installMe(__DIR__)
+ ->apply();
+ }
+
+ /**
+ * Test api applies permissions on line item actions (delete & get).
+ */
+ public function testLineItemApiPermissions() {
+ $contact1 = $this->individualCreate();
+ $defaultPriceFieldID = $this->getDefaultPriceFieldID();
+ $this->callAPISuccess('Order', 'create', [
+ 'financial_type_id' => 'Donation',
+ 'contact_id' => $contact1,
+ 'line_items' => [
+ [
+ 'line_item' => [
+ [
+ 'financial_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Donation'),
+ 'line_total' => 40,
+ 'price_field_id' => $defaultPriceFieldID,
+ 'qty' => 1,
+ ],
+ [
+ 'financial_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Member Dues'),
+ 'line_total' => 50,
+ 'price_field_id' => $defaultPriceFieldID,
+ 'qty' => 1,
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->setPermissions([
+ 'access CiviCRM',
+ 'access CiviContribute',
+ 'edit contributions',
+ 'delete in CiviContribute',
+ 'view contributions of type Donation',
+ 'delete contributions of type Donation',
+ ]);
+ Civi::settings()->set('acl_financial_type', TRUE);
+ $this->createLoggedInUser();
+
+ $lineItems = $this->callAPISuccess('LineItem', 'get', ['sequential' => TRUE])['values'];
+ $this->assertCount(2, $lineItems);
+ $this->callAPISuccessGetCount('LineItem', ['check_permissions' => TRUE], 1);
+
+ $this->callAPISuccess('LineItem', 'Delete', ['check_permissions' => TRUE, 'id' => $lineItems[0]['id']]);
+ $this->callAPIFailure('LineItem', 'Delete', ['check_permissions' => TRUE, 'id' => $lineItems[1]['id']]);
+ }
+
+ /**
+ * Set ACL permissions, overwriting any existing ones.
+ *
+ * @param array $permissions
+ * Array of permissions e.g ['access CiviCRM','access CiviContribute'],
+ */
+ protected function setPermissions($permissions) {
+ CRM_Core_Config::singleton()->userPermissionClass->permissions = $permissions;
+ if (isset(\Civi::$statics['CRM_Financial_BAO_FinancialType'])) {
+ unset(\Civi::$statics['CRM_Financial_BAO_FinancialType']);
+ }
+ }
+
+ /**
+ * @return mixed
+ * @throws \API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ protected function getDefaultPriceFieldID(): int {
+ return PriceField::get()
+ ->addWhere('price_set_id:name', '=', 'default_contribution_amount')
+ ->addWhere('name', '=', 'contribution_amount')
+ ->addWhere('html_type', '=', 'Text')
+ ->addSelect('id')->execute()->first()['id'];
+ }
+
+}
--- /dev/null
+<?php
+
+ini_set('memory_limit', '2G');
+ini_set('safe_mode', 0);
+// phpcs:disable
+eval(cv('php:boot --level=classloader', 'phpcode'));
+// phpcs:enable
+// Allow autoloading of PHPUnit helper classes in this extension.
+$loader = new \Composer\Autoload\ClassLoader();
+$loader->add('CRM_', __DIR__);
+$loader->add('Civi\\', __DIR__);
+$loader->add('api_', __DIR__);
+$loader->add('api\\', __DIR__);
+$loader->register();
+
+/**
+ * Call the "cv" command.
+ *
+ * @param string $cmd
+ * The rest of the command to send.
+ * @param string $decode
+ * Ex: 'json' or 'phpcode'.
+ * @return string
+ * Response output (if the command executed normally).
+ * @throws \RuntimeException
+ * If the command terminates abnormally.
+ */
+function cv($cmd, $decode = 'json') {
+ $cmd = 'cv ' . $cmd;
+ $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
+ $oldOutput = getenv('CV_OUTPUT');
+ putenv("CV_OUTPUT=json");
+
+ // Execute `cv` in the original folder. This is a work-around for
+ // phpunit/codeception, which seem to manipulate PWD.
+ $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd);
+
+ $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
+ putenv("CV_OUTPUT=$oldOutput");
+ fclose($pipes[0]);
+ $result = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ if (proc_close($process) !== 0) {
+ throw new RuntimeException("Command failed ($cmd):\n$result");
+ }
+ switch ($decode) {
+ case 'raw':
+ return $result;
+
+ case 'phpcode':
+ // If the last output is /*PHPCODE*/, then we managed to complete execution.
+ if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
+ throw new \RuntimeException("Command failed ($cmd):\n$result");
+ }
+ return $result;
+
+ case 'json':
+ return json_decode($result, 1);
+
+ default:
+ throw new RuntimeException("Bad decoder format ($decode)");
+ }
+}