diff --git a/dev/tests/functional/_bootstrap.php b/dev/tests/functional/_bootstrap.php
index 51dcf2063..5e6621fe0 100755
--- a/dev/tests/functional/_bootstrap.php
+++ b/dev/tests/functional/_bootstrap.php
@@ -4,7 +4,7 @@
* See COPYING.txt for license details.
*/
-define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__))));
+defined('PROJECT_ROOT') || define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__))));
require_once realpath(PROJECT_ROOT . '/vendor/autoload.php');
//Load constants from .env file
diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php
new file mode 100644
index 000000000..a451f8dc9
--- /dev/null
+++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php
@@ -0,0 +1,38 @@
+ ["$testKey=$testValue"]
+ ]);
+
+ $encryptedCred = CredentialStore::getInstance()->getSecret($testKey);
+
+ // assert the value we've gotten is in fact not identical to our test value
+ $this->assertNotEquals($testValue, $encryptedCred);
+
+ $actualValue = CredentialStore::getInstance()->decryptSecretValue($encryptedCred);
+
+ // assert that we are able to successfully decrypt our secret value
+ $this->assertEquals($testValue, $actualValue);
+ }
+}
diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php
index 183c9226d..f584adea9 100644
--- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php
+++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php
@@ -48,9 +48,9 @@ public function testInvalidMergeOrderReference()
try {
$this->testActionObjectExtractor->extractActions($invalidArray, 'TestWithSelfReferencingStepKey');
} catch (\Exception $e) {
- TestLoggingUtil::getInstance()->validateMockLogStatement(
+ TestLoggingUtil::getInstance()->validateMockLogStatmentRegex(
'error',
- 'Line 108: Invalid ordering configuration in test',
+ '/Line \d*: Invalid ordering configuration in test/',
[
'test' => 'TestWithSelfReferencingStepKey',
'stepKey' => ['invalidTestAction1']
diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php
index 4be16280f..7c2ecc142 100644
--- a/dev/tests/unit/Util/TestLoggingUtil.php
+++ b/dev/tests/unit/Util/TestLoggingUtil.php
@@ -82,6 +82,15 @@ public function validateMockLogStatement($type, $message, $context)
$this->assertEquals($context, $record['context']);
}
+ public function validateMockLogStatmentRegex($type, $regex, $context)
+ {
+ $records = $this->testLogHandler->getRecords();
+ $record = $records[count($records)-1]; // we assume the latest record is what requires validation
+ $this->assertEquals(strtoupper($type), $record['level_name']);
+ $this->assertRegExp($regex, $record['message']);
+ $this->assertEquals($context, $record['context']);
+ }
+
/**
* Function which clears the test logger context from the LogginUtil class. Should be run after a test class has
* executed.
diff --git a/dev/tests/verification/Resources/PersistedReplacementTest.txt b/dev/tests/verification/Resources/PersistedReplacementTest.txt
index f4f9b0298..78cdd5029 100644
--- a/dev/tests/verification/Resources/PersistedReplacementTest.txt
+++ b/dev/tests/verification/Resources/PersistedReplacementTest.txt
@@ -53,7 +53,7 @@ class PersistedReplacementTestCest
$I->fillField("#selector", "StringBefore " . $createdData->getCreatedDataByName('firstname') . " StringAfter");
$I->fillField("#" . $createdData->getCreatedDataByName('firstname'), "input");
$I->fillField("#" . getenv("MAGENTO_BASE_URL") . "#" . $createdData->getCreatedDataByName('firstname'), "input");
- $I->fillField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input");
+ $I->fillSecretField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input");
$I->dragAndDrop("#" . $createdData->getCreatedDataByName('firstname'), $createdData->getCreatedDataByName('lastname'));
$I->conditionalClick($createdData->getCreatedDataByName('lastname'), "#" . $createdData->getCreatedDataByName('firstname'), true);
$I->amOnUrl($createdData->getCreatedDataByName('firstname') . ".html");
diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php
index 173792d7c..728c4cb3a 100644
--- a/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php
+++ b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php
@@ -48,7 +48,9 @@ public function readFiles($fileList)
}
}
$exceptionCollector->throwException();
- $this->validateSchema($configMerger, $fileList->getFilename());
+ if ($fileList->valid()) {
+ $this->validateSchema($configMerger, $fileList->getFilename());
+ }
$output = [];
if ($configMerger) {
diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php
index 4caf4287a..3c06a8ff4 100644
--- a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php
+++ b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php
@@ -13,6 +13,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Debug\Debug;
use Symfony\Component\Process\Process;
class RunTestCommand extends Command
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php
index edd5a7c18..a28547d18 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php
@@ -13,13 +13,29 @@
class CredentialStore
{
+ const ENCRYPTION_ALGO = "AES-256-CBC";
+
/**
- * Singletone instnace
+ * Singleton instance
*
* @var CredentialStore
*/
private static $INSTANCE = null;
+ /**
+ * Initial vector for open_ssl encryption.
+ *
+ * @var string
+ */
+ private $iv = null;
+
+ /**
+ * Key for open_ssl encryption/decryption
+ *
+ * @var string
+ */
+ private $encodedKey = null;
+
/**
* Key/Value paris of credential names and their corresponding values
*
@@ -46,7 +62,10 @@ public static function getInstance()
*/
private function __construct()
{
- $this->readInCredentialsFile();
+ $this->encodedKey = base64_encode(openssl_random_pseudo_bytes(16));
+ $this->iv = substr(hash('sha256', $this->encodedKey), 0, 16);
+ $creds = $this->readInCredentialsFile();
+ $this->credentials = $this->encryptCredFileContents($creds);
}
/**
@@ -77,7 +96,7 @@ public function getSecret($key)
/**
* Private function which reads in secret key/values from .credentials file and stores in memory as key/value pair.
*
- * @return void
+ * @return array
* @throws TestFrameworkException
*/
private function readInCredentialsFile()
@@ -95,7 +114,18 @@ private function readInCredentialsFile()
);
}
- $credContents = file($credsFilePath, FILE_IGNORE_NEW_LINES);
+ return file($credsFilePath, FILE_IGNORE_NEW_LINES);
+ }
+
+ /**
+ * Function which takes the contents of the credentials file and encrypts the entries.
+ *
+ * @param array $credContents
+ * @return array
+ */
+ private function encryptCredFileContents($credContents)
+ {
+ $encryptedCreds = [];
foreach ($credContents as $credValue) {
if (substr($credValue, 0, 1) === '#' || empty($credValue)) {
continue;
@@ -103,8 +133,27 @@ private function readInCredentialsFile()
list($key, $value) = explode("=", $credValue);
if (!empty($value)) {
- $this->credentials[$key] = $value;
+ $encryptedCreds[$key] = openssl_encrypt(
+ $value,
+ self::ENCRYPTION_ALGO,
+ $this->encodedKey,
+ 0,
+ $this->iv
+ );
}
}
+
+ return $encryptedCreds;
+ }
+
+ /**
+ * Takes a value encrypted at runtime and descrypts using the object's initial vector.
+ *
+ * @param string $value
+ * @return string
+ */
+ public function decryptSecretValue($value)
+ {
+ return openssl_decrypt($value, self::ENCRYPTION_ALGO, $this->encodedKey, 0, $this->iv);
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php
index ecfb6cadd..f1aae2487 100644
--- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php
+++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php
@@ -6,25 +6,18 @@
namespace Magento\FunctionalTestingFramework\Module;
-use Codeception\Events;
use Codeception\Module\WebDriver;
use Codeception\Test\Descriptor;
use Codeception\TestInterface;
-use Facebook\WebDriver\WebDriverSelect;
-use Facebook\WebDriver\WebDriverBy;
-use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Interactions\WebDriverActions;
-use Codeception\Exception\ElementNotFound;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleException;
use Codeception\Util\Uri;
-use Codeception\Util\ActionSequence;
+use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiExecutor;
use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport;
use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface;
-use Magento\Setup\Exception;
use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil;
-use Yandex\Allure\Adapter\Event\TestCaseFinishedEvent;
use Yandex\Allure\Adapter\Support\AttachmentSupport;
/**
@@ -44,6 +37,8 @@
* password: admin_password
* browser: chrome
* ```
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class MagentoWebDriver extends WebDriver
{
@@ -596,6 +591,23 @@ public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null)
}
}
+ /**
+ * Function used to fill sensitive crednetials with user data, data is decrypted immediately prior to fill to avoid
+ * exposure in console or log.
+ *
+ * @param string $field
+ * @param string $value
+ * @return void
+ */
+ public function fillSecretField($field, $value)
+ {
+ // to protect any secrets from being printed to console the values are executed only at the webdriver level as a
+ // decrypted value
+
+ $decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value);
+ $this->fillField($field, $decryptedValue);
+ }
+
/**
* Override for _failed method in Codeception method. Adds png and html attachments to allure report
* following parent execution of test failure processing.
diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php
index 5ff2c8803..75970f0dd 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php
@@ -9,6 +9,8 @@
use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler;
use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil;
use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor;
+use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor;
+use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor;
/**
* Class TestObject
@@ -183,12 +185,10 @@ public function getEstimatedDuration()
}
$hookTime = 0;
- if (array_key_exists('before', $this->hooks)) {
- $hookTime += $this->calculateWeightedActionTimes($this->hooks['before']->getActions());
- }
-
- if (array_key_exists('after', $this->hooks)) {
- $hookTime += $this->calculateWeightedActionTimes($this->hooks['after']->getActions());
+ foreach ([TestObjectExtractor::TEST_BEFORE_HOOK, TestObjectExtractor::TEST_AFTER_HOOK] as $hookName) {
+ if (array_key_exists($hookName, $this->hooks)) {
+ $hookTime += $this->calculateWeightedActionTimes($this->hooks[$hookName]->getActions());
+ }
}
$testTime = $this->calculateWeightedActionTimes($this->getOrderedActions());
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php
index 99e575491..fb562c626 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php
@@ -83,7 +83,66 @@ public function resolveActionSteps($parsedSteps, $skipActionGroupResolution = fa
return $this->orderedSteps;
}
- return $this->resolveActionGroups($this->orderedSteps);
+ $resolvedActions = $this->resolveActionGroups($this->orderedSteps);
+ return $this->resolveSecretFieldAccess($resolvedActions);
+ }
+
+ /**
+ * Takes an array of actions and resolves any references to secret fields. The function then validates whether the
+ * refernece is valid and replaces the function name accordingly to hide arguments at runtime.
+ *
+ * @param ActionObject[] $resolvedActions
+ * @return ActionObject[]
+ * @throws TestReferenceException
+ */
+ private function resolveSecretFieldAccess($resolvedActions)
+ {
+ $actions = [];
+ foreach ($resolvedActions as $resolvedAction) {
+ $action = $resolvedAction;
+ $hasSecretRef = $this->actionAttributeContainsSecretRef($resolvedAction->getCustomActionAttributes());
+
+ if ($resolvedAction->getType() !== 'fillField' && $hasSecretRef) {
+ throw new TestReferenceException("You cannot reference secret data outside of fill field actions");
+ }
+
+ if ($resolvedAction->getType() === 'fillField' && $hasSecretRef) {
+ $action = new ActionObject(
+ $action->getStepKey(),
+ 'fillSecretField',
+ $action->getCustomActionAttributes(),
+ $action->getLinkedAction(),
+ $action->getActionOrigin()
+ );
+ }
+
+ $actions[$action->getStepKey()] = $action;
+ }
+
+ return $actions;
+ }
+
+ /**
+ * Returns a boolean based on whether or not the action attributes contain a reference to a secret field.
+ *
+ * @param array $actionAttributes
+ * @return boolean
+ */
+ private function actionAttributeContainsSecretRef($actionAttributes)
+ {
+ foreach ($actionAttributes as $actionAttribute) {
+ if (is_array($actionAttribute)) {
+ return $this->actionAttributeContainsSecretRef($actionAttribute);
+ }
+
+ preg_match_all("/{{_CREDS\.([\w]+)}}/", $actionAttribute, $matches);
+
+ if (!empty($matches[0])) {
+ return true;
+ }
+ }
+
+ return false;
}
/**
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php
index 7642aec31..5cd7b7ac5 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php
@@ -59,6 +59,7 @@ public function extractActions($testActions, $testName = null)
foreach ($testActions as $actionName => $actionData) {
$stepKey = $actionData[self::TEST_STEP_MERGE_KEY];
+ $actionType = $actionData[self::NODE_NAME];
if (empty($stepKey)) {
throw new XmlException(sprintf(self::STEP_KEY_EMPTY_ERROR_MSG, $actionData['nodeName']));
@@ -79,10 +80,7 @@ public function extractActions($testActions, $testName = null)
$actionAttributes['parameterArray'] = $actionData['array']['value'];
}
- if ($actionData[self::NODE_NAME] === self::ACTION_GROUP_TAG) {
- $actionAttributes = $this->processActionGroupArgs($actionAttributes);
- }
-
+ $actionAttributes = $this->processActionGroupArgs($actionType, $actionAttributes);
$linkedAction = $this->processLinkedActions($actionName, $actionData);
$actions = $this->extractFieldActions($actionData, $actions);
$actionAttributes = $this->extractFieldReferences($actionData, $actionAttributes);
@@ -98,7 +96,7 @@ public function extractActions($testActions, $testName = null)
$actions[$stepKey] = new ActionObject(
$stepKey,
- $actionData[self::NODE_NAME],
+ $actionType,
$actionAttributes,
$linkedAction['stepKey'],
$linkedAction['order']
@@ -142,11 +140,16 @@ private function processLinkedActions($actionName, $actionData)
* Takes the action group reference and parses out arguments as an array that can be passed to override defaults
* defined in the action group xml.
*
- * @param array $actionAttributeData
+ * @param string $actionType
+ * @param array $actionAttributeData
* @return array
*/
- private function processActionGroupArgs($actionAttributeData)
+ private function processActionGroupArgs($actionType, $actionAttributeData)
{
+ if ($actionType !== self::ACTION_GROUP_TAG) {
+ return $actionAttributeData;
+ }
+
$actionAttributeArgData = [];
foreach ($actionAttributeData as $attributeDataKey => $attributeDataValues) {
if ($attributeDataKey == self::ACTION_GROUP_REF) {
diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd
index 9aa772858..90671c340 100644
--- a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd
+++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd
@@ -37,6 +37,7 @@
+
diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php
index 99baeba41..45b9883ea 100644
--- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php
+++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php
@@ -210,7 +210,7 @@ private function assembleTestPhp($testObject)
$hookPhp = $this->generateHooksPhp($testObject->getHooks());
$testsPhp = $this->generateTestPhp($testObject);
} catch (TestReferenceException $e) {
- throw new TestReferenceException($e->getMessage() . " in Test \"" . $testObject->getName() . "\"");
+ throw new TestReferenceException($e->getMessage() . "\n" . $testObject->getFilename());
}
$cestPhp = "