verification/TestModule/Test/ActionGroupFunctionalTest.xml ")
*/
class ActionGroupWithDataTestCest
{
diff --git a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt
index 289bbe4e1..ca5f72cc0 100644
--- a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt
+++ b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt
@@ -14,7 +14,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId;
/**
* @Title("[NO TESTCASEID]: Action Group With Default Argument Value and Hardcoded Value in Param")
- * @Description("
verification/TestModule/Test/ActionGroupTest.xml ")
*/
class ActionGroupWithParameterizedElementsWithStepKeyReferencesCest
{
diff --git a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt
index 254fbe0f2..e816da90b 100644
--- a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt
+++ b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt
@@ -14,7 +14,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId;
/**
* @Title("[NO TESTCASEID]: Action Group With Passed Argument Value and Hardcoded Value in Param")
- * @Description("
verification/TestModule/Test/ActionGroupTest.xml ")
*/
class ActionGroupWithSimpleDataUsageFromPassedArgumentCest
{
diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt
index 5d8298108..c23b99cab 100644
--- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt
+++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt
@@ -14,7 +14,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId;
/**
* @Title("[NO TESTCASEID]: Action Group With Default Argument Value and Argument Value in Param")
- * @Description("
verification/TestModule/Test/ActionGroupTest.xml ")
*/
class ActionGroupWithSingleParameterSelectorFromDefaultArgumentCest
{
diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt
index 283a9fdd7..4a0932b99 100644
--- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt
+++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt
@@ -14,7 +14,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId;
/**
* @Title("[NO TESTCASEID]: Action Group With Passed Argument Value and Argument Value in Param")
- * @Description("
verification/TestModule/Test/XmlCommentedActionGroupTest.xml ")
*/
class XmlCommentedActionGroupTestCest
{
diff --git a/dev/tests/verification/Resources/XmlCommentedTest.txt b/dev/tests/verification/Resources/XmlCommentedTest.txt
index 5b8057099..47d984d9e 100644
--- a/dev/tests/verification/Resources/XmlCommentedTest.txt
+++ b/dev/tests/verification/Resources/XmlCommentedTest.txt
@@ -14,7 +14,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId;
/**
* @Title("[NO TESTCASEID]: Test With comment blocks in root element 'tests', in annotations and in test body.")
- * @Description("
verification/TestModule/Test/XmlCommentedTest.xml ")
*/
class XmlCommentedTestCest
{
diff --git a/dev/tests/verification/TestModule/ActionGroup/DeprecatedActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/DeprecatedActionGroup.xml
new file mode 100644
index 000000000..8f0341c09
--- /dev/null
+++ b/dev/tests/verification/TestModule/ActionGroup/DeprecatedActionGroup.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/verification/TestModule/Data/DeprecatedData.xml b/dev/tests/verification/TestModule/Data/DeprecatedData.xml
new file mode 100644
index 000000000..060752419
--- /dev/null
+++ b/dev/tests/verification/TestModule/Data/DeprecatedData.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ deprecated
+
+
diff --git a/dev/tests/verification/TestModule/Page/DeprecatedPage.xml b/dev/tests/verification/TestModule/Page/DeprecatedPage.xml
new file mode 100644
index 000000000..316b8b1d7
--- /dev/null
+++ b/dev/tests/verification/TestModule/Page/DeprecatedPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/dev/tests/verification/TestModule/Section/DeprecatedSection.xml b/dev/tests/verification/TestModule/Section/DeprecatedSection.xml
new file mode 100644
index 000000000..a9ed20d98
--- /dev/null
+++ b/dev/tests/verification/TestModule/Section/DeprecatedSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml b/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml
index c23a3ce60..1ef25b43d 100644
--- a/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml
+++ b/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml
@@ -79,6 +79,9 @@
+
+
+
@@ -143,4 +146,4 @@
-
\ No newline at end of file
+
diff --git a/dev/tests/verification/TestModule/Test/DeprecatedEntitiesTest.xml b/dev/tests/verification/TestModule/Test/DeprecatedEntitiesTest.xml
new file mode 100644
index 000000000..733a34e80
--- /dev/null
+++ b/dev/tests/verification/TestModule/Test/DeprecatedEntitiesTest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/verification/TestModule/Test/DeprecatedTest.xml b/dev/tests/verification/TestModule/Test/DeprecatedTest.xml
new file mode 100644
index 000000000..cc5ae4994
--- /dev/null
+++ b/dev/tests/verification/TestModule/Test/DeprecatedTest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/verification/Tests/DeprecatedTest.php b/dev/tests/verification/Tests/DeprecatedTest.php
new file mode 100644
index 000000000..78e3326b9
--- /dev/null
+++ b/dev/tests/verification/Tests/DeprecatedTest.php
@@ -0,0 +1,33 @@
+generateAndCompareTest('DeprecatedTest');
+ }
+
+ /**
+ * Tests flat generation of a test which uses deprecated entities.
+ *
+ * @throws \Exception
+ * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException
+ */
+ public function testDeprecatedEntitiesOnlyGeneration()
+ {
+ $this->generateAndCompareTest('DeprecatedEntitiesTest');
+ }
+}
diff --git a/docs/best-practices.md b/docs/best-practices.md
index a3e5f2629..0b95b1c41 100644
--- a/docs/best-practices.md
+++ b/docs/best-practices.md
@@ -5,12 +5,12 @@ Check out our best practices below to ensure you are getting the absolute most o
## Action group
1. [Action group] names should be sufficiently descriptive to inform a test writer of what the action group does and when it should be used.
- Add additional explanation in comments if needed.
+ Add additional explanation in annotations if needed.
2. Provide default values for the arguments that apply to your most common case scenarios.
## `actionGroups` vs `extends`
-Use an action group to wraps a set of actions to reuse them multiple times.
+Use an action group to wrap a set of actions to reuse them multiple times.
Use an [extension] when a test or action group needs to be repeated with the exception of a few steps.
@@ -58,12 +58,20 @@ The following pattern is used when merging with `extends`:
Name files according to the following patterns to make searching in future more easy:
+
+
#### Test file name
Format: {_Admin_ or _Storefront_}{Functionality}_Test.xml_, where Functionality briefly describes the testing functionality.
Example: _StorefrontCreateCustomerTest.xml_.
+#### Action Group file name
+
+Format: {_Admin_ or _Storefront_}{Action Group Summary}ActionGroup.xml`, where Action Group Summary describes with a few words what we can expect from it.
+
+Example: _AdminCreateStoreActionGroup.xml_
+
#### Section file name
Format: {_Admin_ or _Storefront_}{UI Description}_Section.xml_, where UI Description briefly describes the testing UI.
@@ -74,6 +82,8 @@ Example: _AdminNavbarSection.xml_.
Format: {Type}_Data.xml_, where Type represents the entity type.
+
+
Example: _ProductData.xml_.
### Object names
@@ -85,18 +95,19 @@ Use the _Foo.camelCase_ naming convention, which is similar to _Classes_ and _cl
Use an upper case first letter for:
- File names. Example: _StorefrontCreateCustomerTest.xml_
-- Test name attributes. Example: ``.
-- Data entity names. Example: ``.
-- Page name. Example: ``.
-- Section name. Example: ``.
-- Action group name. Example: ``.
+- Test name attributes. Example: ``
+- Data entity names. Example: ``
+- Page name. Example: ``
+- Section name. Example: ``
+- Action group name. Example: ``
#### Lower case
Use a lower case first letter for:
-- Data keys. Example: ``.
-- Element names. Examples: ``.
+- Data keys. Example: ``
+- Element names. Examples: ``
+- Step keys. For example: ``
## Page object
diff --git a/docs/commands/mftf.md b/docs/commands/mftf.md
index 2440428f9..b2c36dae4 100644
--- a/docs/commands/mftf.md
+++ b/docs/commands/mftf.md
@@ -42,6 +42,12 @@ vendor/bin/mftf generate:tests
vendor/bin/mftf generate:tests AdminLoginTest StorefrontPersistedCustomerLoginTest
```
+### Generate test by test and suite name
+
+```bash
+vendor/bin/mftf generate:tests LoginSuite:AdminLoginTest
+```
+
### Generate and run the tests for a specified group
```bash
@@ -58,6 +64,14 @@ vendor/bin/mftf run:test AdminLoginTest StorefrontPersistedCustomerLoginTest -r
This command cleans up the previously generated tests; generates and runs the `LoginAsAdminTest` and `LoginAsCustomerTest` tests.
+### Generate and run particular test in a specific suite's context
+
+```bash
+vendor/bin/mftf run:test LoginSuite:AdminLoginTest -r
+```
+
+This command cleans up previously generated tests; generates and run `AdminLoginTest` within the context of the `LoginSuite`.
+
### Generate and run a testManifest.txt file
```bash
@@ -449,18 +463,46 @@ The example parameters are taken from the `etc/config/.env.example` file.
### `static-checks`
-Runs all MFTF static-checks on the test codebase that MFTF is currently attached to.
+Runs all or specific MFTF static-checks on the test codebase that MFTF is currently attached to.
+If no script name argument is specified, all existing static check scripts will run.
-#### Existing static checks
+#### Usage
-* Test Dependency: Checks that test dependencies do not violate Magento module's composer dependencies.
+```bash
+vendor/bin/mftf static-checks []...
+```
-#### Usage
+#### Examples
+
+To check what existing static check scripts are available
+
+```bash
+vendor/bin/mftf static-checks --help
+```
+
+To run all existing static check scripts
```bash
vendor/bin/mftf static-checks
```
+To run specific static check scripts
+
+```bash
+vendor/bin/mftf static-checks testDependencies
+```
+```bash
+vendor/bin/mftf static-checks actionGroupArguments
+```
+```bash
+vendor/bin/mftf static-checks testDependencies actionGroupArguments
+```
+
+#### Existing static checks
+
+* Test Dependency: Checks that test dependencies do not violate Magento module's composer dependencies.
+* Action Group Unused Arguments: Checks that action groups do not have unused arguments.
+
### `upgrade:tests`
Applies all the MFTF major version upgrade scripts to test components in the given path (`test.xml`, `data.xml`, etc).
diff --git a/docs/configuration.md b/docs/configuration.md
index 5140c01e7..9466f2bcc 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -277,6 +277,28 @@ Example:
CREDENTIAL_VAULT_SECRET_BASE_PATH=secret
```
+### CREDENTIAL_AWS_SECRETS_MANAGER_REGION
+
+The region that AWS Secrets Manager is located.
+
+Example:
+
+```conf
+# Region of AWS Secrets Manager
+CREDENTIAL_AWS_SECRETS_MANAGER_REGION=us-east-1
+```
+
+### CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE
+
+The profile used to connect to AWS Secrets Manager.
+
+Example:
+
+```conf
+# Profile used to connect to AWS Secrets Manager.
+CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default
+```
+
### ENABLE_BROWSER_LOG
Enables addition of browser logs to Allure steps
diff --git a/docs/credentials.md b/docs/credentials.md
index a2850cfe8..402030985 100644
--- a/docs/credentials.md
+++ b/docs/credentials.md
@@ -3,10 +3,11 @@
When you test functionality that involves external services such as UPS, FedEx, PayPal, or SignifyD,
use the MFTF credentials feature to hide sensitive [data][] like integration tokens and API keys.
-Currently the MFTF supports two types of credential storage:
+Currently the MFTF supports three types of credential storage:
- **.credentials file**
-- **HashiCorp vault**
+- **HashiCorp Vault**
+- **AWS Secrets Manager**
## Configure File Storage
@@ -135,11 +136,100 @@ CREDENTIAL_VAULT_ADDRESS=http://127.0.0.1:8200
CREDENTIAL_VAULT_SECRET_BASE_PATH=secret
```
-## Configure both File Storage and Vault Storage
+## Configure AWS Secrets Manager
-It is possible and sometimes useful to setup and use both `.credentials` file and vault for secret storage at the same time.
-In this case, the MFTF tests are able to read secret data at runtime from both storage options, but the local `.credentials` file will take precedence.
+AWS Secrets Manager offers secret management that supports:
+- Secret rotation with built-in integration for Amazon RDS, Amazon Redshift, and Amazon DocumentDB
+- Fine-grained policies and permissions
+- Audit secret rotation centrally for resources in the AWS Cloud, third-party services, and on-premises
+### Prerequisites
+
+#### Use AWS Secrets Manager from your own AWS account
+
+- An AWS account with Secrets Manager service
+- An IAM user with AWS Secrets Manager access permission
+
+#### Use AWS Secrets Manager in CI/CD
+
+- AWS account ID where the AWS Secrets Manager service is hosted
+- Authorized CI/CD EC2 instances with AWS Secrets Manager service access IAM role attached
+
+### Store secrets in AWS Secrets Manager
+
+#### Secrets format
+
+`Secret Name` and `Secret Value` are two key pieces of information for creating a secret.
+
+`Secret Value` can be either plaintext or key/value pairs in JSON format.
+
+`Secret Name` must use the following format:
+
+```conf
+mftf//
+```
+
+`Secret Value` can be stored in two different formats: plaintext or key/value pairs.
+
+For plaintext format, `Secret Value` can be any string you want to secure.
+
+For key/value pairs format, `Secret Value` is a key/value pair with `key` the same as `Secret Name` without `mftf//` prefix, which is ``, and value can be any string you want to secure.
+
+##### Create Secrets using AWS CLI
+
+```bash
+aws secretsmanager create-secret --name "mftf/magento/shipping/carriers_usps_userid" --description "Carriers USPS user id" --secret-string "1234567"
+```
+
+##### Create Secrets using AWS Console
+
+- Sign in to the AWS Secrets Manager console
+- Choose Store a new secret
+- In the Select secret type section, specify "Other type of secret"
+- For `Secret Name`, `Secret Key` and `Secret Value` field, for example, to save the same secret in key/value JSON format, you should use
+
+```conf
+# Secret Name
+mftf/magento/shipping/carriers_usps_userid
+
+# Secret Key
+shipping/carriers_usps_userid
+
+# Secret Value
+1234567
+```
+
+### Setup MFTF to use AWS Secrets Manager
+
+To use AWS Secrets Manager, the AWS region to connect to is required. You can set it through environment variable [`CREDENTIAL_AWS_SECRETS_MANAGER_REGION`][] in `.env`.
+
+MFTF uses the recommended [Default Credential Provider Chain][credential chain] to establish connection to AWS Secrets Manager service.
+You can setup credentials according to [Default Credential Provider Chain][credential chain] and there is no MFTF specific setup required.
+Optionally, however, you can explicitly set AWS profile through environment variable [`CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE`][] in `.env`.
+
+```conf
+# Sample AWS Secrets Manager configuration
+CREDENTIAL_AWS_SECRETS_MANAGER_REGION=us-east-1
+CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default
+```
+
+### Optionally set CREDENTIAL_AWS_ACCOUNT_ID environment variable
+
+In case AWS credentials cannot resolve to a valid AWS account, full AWS KMS ([Key Management Service][]) key ARN ([Amazon Resource Name][]) is required.
+You will also need to set `CREDENTIAL_AWS_ACCOUNT_ID` environment variable so that MFTF can construct the full ARN. This is mostly used for CI/CD.
+
+```bash
+export CREDENTIAL_AWS_ACCOUNT_ID=
+```
+
+## Configure multiple credential storage
+
+It is possible and sometimes useful to setup and use multiple credential storage at the same time.
+In this case, the MFTF tests are able to read secret data at runtime from all storage options, in this case MFTF use the following precedence:
+
+```
+.credentials File > HashiCorp Vault > AWS Secrets Manager
+```
## Use credentials in a test
@@ -150,7 +240,7 @@ Define the value as a reference to the corresponding key in the credentials file
- `_CREDS` is an environment constant pointing to the `.credentials` file
- `my_data_key` is a key in the the `.credentials` file or vault that contains the value to be used in a test step
- - for File Storage, ensure your key contains the vendor prefix, i.e. `vendor/my_data_key`
+ - for File Storage, ensure your key contains the vendor prefix, which is `vendor/my_data_key`
For example, to reference secret data in the [`fillField`][] action, use the `userInput` attribute using a typical File Storage:
@@ -183,3 +273,8 @@ The MFTF tests delivered with Magento application do not use credentials and do
[Vault KV2]: https://www.vaultproject.io/docs/secrets/kv/kv-v2.html
[`CREDENTIAL_VAULT_ADDRESS`]: configuration.md#credential_vault_address
[`CREDENTIAL_VAULT_SECRET_BASE_PATH`]: configuration.md#credential_vault_secret_base_path
+[credential chain]: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials.html
+[`CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE`]: configuration.md#credential_aws_secrets_manager_profile
+[`CREDENTIAL_AWS_SECRETS_MANAGER_REGION`]: configuration.md#credential_aws_secrets_manager_region
+[Key Management Service]: https://aws.amazon.com/kms/
+[Amazon Resource Name]: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
\ No newline at end of file
diff --git a/docs/extending.md b/docs/extending.md
index 91dc97c5d..064ed3208 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -26,7 +26,7 @@ __Use case__: Create two similar tests with different `url` (`"{{AdminCategoryPa
> Test with "extends":
```xml
-
+
...
@@ -47,7 +47,7 @@ __Use case__: Create two similar tests with different `url` (`"{{AdminCategoryPa
> Test without "extends":
```xml
-
+
...
@@ -77,7 +77,7 @@ __Use case__: Create two similar tests where the second test contains two additi
> Tests with "extends":
```xml
-
+
@@ -95,7 +95,7 @@ __Use case__: Create two similar tests where the second test contains two additi
> Tests without "extends":
```xml
-
+
@@ -125,7 +125,7 @@ __Use case__: Create two similar tests where the second one contains two additio
> Tests with "extends":
```xml
-
+
@@ -147,7 +147,7 @@ __Use case__: Create two similar tests where the second one contains two additio
> Tests without "extends":
```xml
-
+
@@ -295,7 +295,7 @@ __Use case__: Create an entity named `DivPanelGreen`, which is similar to the `D
> Entities with "extends":
```xml
-
+
Red
80px
@@ -310,7 +310,7 @@ __Use case__: Create an entity named `DivPanelGreen`, which is similar to the `D
> Entities without "extends":
```xml
-
+
Red
80px
@@ -331,7 +331,7 @@ __Use case__: Create an entity named `DivPanelGreen`, which is similar to the `D
> Entities with "extends":
```xml
-
+
Red
80px
@@ -347,7 +347,7 @@ __Use case__: Create an entity named `DivPanelGreen`, which is similar to the `D
> Entities without "extends":
```xml
-
+
Red
80px
diff --git a/docs/guides/selectors.md b/docs/guides/selectors.md
index 3e019bb75..d1441865a 100644
--- a/docs/guides/selectors.md
+++ b/docs/guides/selectors.md
@@ -208,7 +208,7 @@ Here is an example of what NOT to do, but this demonstrates how the selector wor
In the BAD example above, we are specifying a very precise path to an input element in the DOM, starting from the very top of the document.
-Similarly, the relative XPath selector is a double forward slash `//`. It is used to start searching for an element anywhere in the DOM.
+Similarly, the relative XPath selector is a double forward slash `//`. It is used to start searching for an element anywhere in the DOM starting from the specified element. If no element is defined, the entire DOM is searched.
Example:
@@ -216,6 +216,8 @@ Example:
//div[@class=’form-group’]//input[@id='user-message']
```
+In the `GOOD` example above, all `` elements in the DOM are matched first. Then all `` with `` as one of its parents are matched. The parent does not have to immediately precede it since it uses another double forward slash `//`.
+
#### Parent Selectors
The parent selector (`..`) allows you to jump to the parent element.
diff --git a/docs/mftf-tests.md b/docs/mftf-tests.md
index b9d94baa9..4505975a9 100644
--- a/docs/mftf-tests.md
+++ b/docs/mftf-tests.md
@@ -5,7 +5,6 @@
dl dt{
font-weight:400;
}
-
# MFTF functional test reference
diff --git a/docs/test/action-groups.md b/docs/test/action-groups.md
index 0743481f6..5fdfbb522 100644
--- a/docs/test/action-groups.md
+++ b/docs/test/action-groups.md
@@ -10,8 +10,10 @@ The following diagram shows the structure of an MFTF action group:
The following conventions apply to MFTF action groups:
-- All action groups are declared in XML files and stored in the `/ActionGroup/` directory.
-- Every file name ends with `ActionGroup`, such as `LoginToAdminActionGroup`.
+- All action groups are declared in XML files and stored in the `/Test/Mftf/ActionGroup/` directory.
+- Every file name ends with `ActionGroup` suffix. For exampe `LoginAsAdminActionGroup.xml`.
+- Action group name should be the same as file name without extension.
+- Single file should contain only one `` node
The XML format for the `actionGroups` declaration is:
@@ -34,32 +36,31 @@ The XML format for the `actionGroups` declaration is:
These examples build a declaration for a group of actions that grant authorization to the Admin area, and use the declaration in a test.
-The _Backend/ActionGroup/LoginToAdminActionGroup.xml_ `` relates to the functionality of the _Backend_ module.
-In [test][], the name and identifier of the `` is used as a reference in the `ref` parameter, such as `ref="LoginToAdminActionGroup"`.
+The _Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml_ `` relates to the functionality of the _Magento_Backend_ module.
+
+In [test][], the name and identifier of the `` is used as a reference in the `ref` parameter, such as `ref="LoginAsAdminActionGroup"`.
### Create an action group declaration
To create the `` declaration:
-1. Begin with a _Backend/ActionGroup/LoginToAdminActionGroup.xml_ template for the ``:
+1. Begin with a template for the ``:
```xml
-
- ...
+
+
```
-
-
1. Add actions to the `actionGroup` arguments:
```xml
-
+
@@ -81,14 +82,20 @@ To create the `` declaration:
-
-
-
-
-
-
-
-
+
+
+ Login to Backend Admin using provided User Data. PLEASE NOTE: This Action Group does NOT validate that you are Logged In.
+
+
+
+
+
+
+
+
+
+
+
```
@@ -97,23 +104,23 @@ To create the `` declaration:
In this test example, we want to add the following set of actions:
```xml
-
-
-
+
+
+
```
-Instead of adding this set of actions, use the _LoginToAdminActionGroup_ `` declaration in tests:
+Instead of adding this set of actions, use the _LoginAsAdminActionGroup_ `` declaration in tests:
-1. Reference the `LoginToAdminActionGroup` action group:
+1. Reference the `LoginAsAdminActionGroup` action group:
```xml
-
+
```
1. Update the argument name/value pair to `adminUser` and `CustomAdminUser`:
```xml
-
+
```
@@ -196,30 +203,34 @@ Starting with an action group such as:
```
It can be reworked into more manageable pieces, as below. These smaller steps are easier to read, update, and reuse.
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
+* GoToCategoryGridAndAddNewCategory
+ ```xml
+
+
+
+
+
+ ```
+* FillInBasicCategoryFields
+ ```xml
+
+
+
+
+
+
+
+
+ ```
+* SaveAndVerifyCategoryCreation
+ ```xml
+
+
+
+
+
+
+ ```
@@ -261,4 +272,4 @@ Attribute|Type|Use|Description
[actions]: ./actions.md
[test]: ../test.md
[`argument`]: #argument-tag
-[created]: ../data.md#persist-data
\ No newline at end of file
+[created]: ../data.md#persist-data
diff --git a/docs/test/actions.md b/docs/test/actions.md
index c5dc83fdb..59fd12aa2 100644
--- a/docs/test/actions.md
+++ b/docs/test/actions.md
@@ -1274,6 +1274,29 @@ Attribute|Type|Use|Description
```
+### magentoCron
+
+Used to execute Magento Cron jobs. Groups may be provided optionally. Internal mechanism of `` ensures that Cron Job of single group is ran with 60 seconds interval.
+
+Attribute|Type|Use|Description
+---|---|---|---
+`groups`|string |optional| Run only specified groups of Cron Jobs
+`arguments`|string |optional| Unescaped arguments to be passed in with the CLI command.
+`timeout`|string|optional| Number of seconds CLI command can run without outputting anything.
+`stepKey`|string|required| A unique identifier of the action.
+`before`|string|optional| `stepKey` of action that must be executed next.
+`after`|string|optional| `stepKey` of preceding action.
+
+
+#### Example
+```xml
+
+
+
+
+
+```
+
### makeScreenshot
See [makeScreenshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#makeScreenshot).
diff --git a/etc/config/.env.example b/etc/config/.env.example
index 7320d8b8b..f5b6ef40e 100644
--- a/etc/config/.env.example
+++ b/etc/config/.env.example
@@ -30,10 +30,14 @@ BROWSER=chrome
#MAGENTO_RESTAPI_SERVER_PORT=8080
#MAGENTO_RESTAPI_SERVER_PROTOCOL=https
-#*** Uncomment and set vault address and secret base path if you want to use vault to manage _CREDS secrets ***#
+#*** To use HashiCorp Vault to manage _CREDS secrets, uncomment and set vault address and secret base path ***#
#CREDENTIAL_VAULT_ADDRESS=http://127.0.0.1:8200
#CREDENTIAL_VAULT_SECRET_BASE_PATH=secret
+#*** To use AWS Secrets Manager to manage _CREDS secrets, uncomment and set region, profile is optional, when omitted, AWS default credential provider chain will be used ***#
+#CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default
+#CREDENTIAL_AWS_SECRETS_MANAGER_REGION=us-east-1
+
#*** Uncomment these properties to set up a dev environment with symlinked projects ***#
#TESTS_BP=
#FW_BP=
diff --git a/etc/di.xml b/etc/di.xml
index 3e6313bf3..e5e31cf87 100644
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -8,7 +8,7 @@
+
]>
diff --git a/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php b/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php
index 8c50f0670..90f04abae 100644
--- a/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php
+++ b/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php
@@ -7,17 +7,27 @@
namespace Magento\FunctionalTestingFramework\Codeception\Subscriber;
use Codeception\Event\StepEvent;
+use Codeception\Event\TestEvent;
use Codeception\Lib\Console\Message;
use Codeception\Step;
use Codeception\Step\Comment;
use Codeception\Test\Interfaces\ScenarioDriven;
use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject;
use Magento\FunctionalTestingFramework\Test\Objects\ActionObject;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
use Magento\FunctionalTestingFramework\Util\TestGenerator;
use Symfony\Component\Console\Formatter\OutputFormatter;
+/**
+ * @SuppressWarnings(PHPMD)
+ */
class Console extends \Codeception\Subscriber\Console
{
+ /**
+ * Regular expresion to find deprecated notices.
+ */
+ const DEPRECATED_NOTICE = '/
(?.*?)<\/li>/m';
+
/**
* Test files cache.
*
@@ -53,6 +63,36 @@ public function __construct($extensionOptions = [], $options = [])
parent::__construct($options);
}
+ /**
+ * Triggered event before each test.
+ *
+ * @param TestEvent $e
+ * @return void
+ * @throws \Exception
+ */
+ public function startTest(TestEvent $e)
+ {
+ $test = $e->getTest()->getTestClass();
+ try {
+ $testReflection = new \ReflectionClass($test);
+ $isDeprecated = preg_match_all(self::DEPRECATED_NOTICE, $testReflection->getDocComment(), $match);
+ if ($isDeprecated) {
+ $this->message('DEPRECATION NOTICE(S): ')
+ ->style('debug')
+ ->writeln();
+ foreach ($match['deprecatedMessage'] as $deprecatedMessage) {
+ $this->message(' - ' . $deprecatedMessage)
+ ->style('debug')
+ ->writeln();
+ }
+ }
+ } catch (\ReflectionException $e) {
+ LoggingUtil::getInstance()->getLogger(self::class)->error($e->getMessage(), $e->getTrace());
+ }
+
+ parent::startTest($e);
+ }
+
/**
* Printing stepKey in before step action.
*
diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php
index 2f2cb8ad3..db96b010c 100644
--- a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php
+++ b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php
@@ -235,7 +235,8 @@ protected function validateSchema($configMerger, $filename = null)
$error = str_replace(PHP_EOL, "", $error);
LoggingUtil::getInstance()->getLogger(Filesystem::class)->criticalFailure(
"Schema validation error ",
- ($filename ? [ "file"=> $filename, "error" => $error]: ["error" => $error])
+ ($filename ? [ "file"=> $filename, "error" => $error]: ["error" => $error]),
+ true
);
}
throw new \Exception("Schema validation errors found in xml file(s)" . $filename);
diff --git a/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php b/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php
index 0953e9d68..a0d94b16e 100644
--- a/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php
+++ b/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php
@@ -87,6 +87,10 @@ protected function getTestAndSuiteConfiguration(array $tests)
$suiteToTestPair = [];
foreach($tests as $test) {
+ if (strpos($test, ':') !== false) {
+ $suiteToTestPair[] = $test;
+ continue;
+ }
if (array_key_exists($test, $testsReferencedInSuites)) {
$suites = $testsReferencedInSuites[$test];
foreach ($suites as $suite) {
diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php
index 98d121d40..6ea37785d 100644
--- a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php
+++ b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php
@@ -94,21 +94,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$command->run(new ArrayInput($args), $output);
}
- $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional --verbose --steps';
+ $commandString = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional --verbose --steps';
+ $exitCode = -1;
+ $returnCodes = [];
foreach ($groups as $group) {
- $codeceptionCommand .= " -g {$group}";
- }
+ $codeceptionCommandString = $commandString . " -g {$group}";
+
+ $process = new Process($codeceptionCommandString);
+ $process->setWorkingDirectory(TESTS_BP);
+ $process->setIdleTimeout(600);
+ $process->setTimeout(0);
- $process = new Process($codeceptionCommand);
- $process->setWorkingDirectory(TESTS_BP);
- $process->setIdleTimeout(600);
- $process->setTimeout(0);
+ $returnCodes[] = $process->run(
+ function ($type, $buffer) use ($output) {
+ $output->write($buffer);
+ }
+ );
+ }
- return $process->run(
- function ($type, $buffer) use ($output) {
- $output->write($buffer);
+ foreach ($returnCodes as $returnCode) {
+ if ($returnCode != 0) {
+ return $returnCode;
}
- );
+ $exitCode = 0;
+ }
+ return $exitCode;
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php
index 9757f494e..30c9cd600 100644
--- a/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php
+++ b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php
@@ -8,10 +8,12 @@
namespace Magento\FunctionalTestingFramework\Console;
+use Magento\FunctionalTestingFramework\StaticCheck\StaticCheckInterface;
use Magento\FunctionalTestingFramework\StaticCheck\StaticChecksList;
-use Magento\FunctionalTestingFramework\StaticCheck\StaticCheckListInterface;
use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Exception;
@@ -19,11 +21,18 @@
class StaticChecksCommand extends Command
{
/**
- * Pool of static check scripts to run
+ * Pool of all existing static check objects
*
- * @var StaticCheckListInterface
+ * @var StaticCheckInterface[]
*/
- private $staticChecksList;
+ private $allStaticCheckObjects;
+
+ /**
+ * Static checks to run
+ *
+ * @var StaticCheckInterface[]
+ */
+ private $staticCheckObjects;
/**
* Configures the current command.
@@ -32,13 +41,22 @@ class StaticChecksCommand extends Command
*/
protected function configure()
{
+ $list = new StaticChecksList();
+ $this->allStaticCheckObjects = $list->getStaticChecks();
+ $staticCheckNames = implode(', ', array_keys($this->allStaticCheckObjects));
+ $description = "This command will run all static checks on xml test materials. "
+ . "Available static check scripts are:\n{$staticCheckNames}";
$this->setName('static-checks')
- ->setDescription('This command will run all static checks on xml test materials.');
- $this->staticChecksList = new StaticChecksList();
+ ->setDescription($description)
+ ->addArgument(
+ 'names',
+ InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+ 'name(s) of specific static check script(s) to run'
+ );
}
/**
- *
+ * Run required static check scripts
*
* @param InputInterface $input
* @param OutputInterface $output
@@ -47,11 +65,23 @@ protected function configure()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
- $staticCheckObjects = $this->staticChecksList->getStaticChecks();
+ try {
+ $this->validateInputArguments($input, $output);
+ } catch (InvalidArgumentException $e) {
+ LoggingUtil::getInstance()->getLogger(StaticChecksCommand::class)->error($e->getMessage());
+ $output->writeln($e->getMessage() . " Please fix input arguments and rerun.");
+ return 1;
+ }
$errors = [];
+ foreach ($this->staticCheckObjects as $name => $staticCheck) {
+ LoggingUtil::getInstance()->getLogger(get_class($staticCheck))->info(
+ "\nRunning static check script for: " . $name
+ );
+ $output->writeln(
+ "\nRunning static check script for: " . $name
+ );
- foreach ($staticCheckObjects as $staticCheck) {
$staticCheck->execute($input);
$staticOutput = $staticCheck->getOutput();
@@ -66,4 +96,38 @@ protected function execute(InputInterface $input, OutputInterface $output)
return 1;
}
}
+
+ /**
+ * Validate input arguments
+ *
+ * @param InputInterface $input
+ * @return void
+ * @throws InvalidArgumentException
+ */
+ private function validateInputArguments(InputInterface $input)
+ {
+ $this->staticCheckObjects = [];
+ $requiredChecksNames = $input->getArgument('names');
+ $invalidCheckNames = [];
+ // Found user required static check script(s) to run,
+ // If no static check name is supplied, run all static check scripts
+ if (empty($requiredChecksNames)) {
+ $this->staticCheckObjects = $this->allStaticCheckObjects;
+ } else {
+ for ($index = 0; $index < count($requiredChecksNames); $index++) {
+ if (in_array($requiredChecksNames[$index], array_keys($this->allStaticCheckObjects))) {
+ $this->staticCheckObjects[$requiredChecksNames[$index]] =
+ $this->allStaticCheckObjects[$requiredChecksNames[$index]];
+ } else {
+ $invalidCheckNames[] = $requiredChecksNames[$index];
+ }
+ }
+ }
+
+ if (!empty($invalidCheckNames)) {
+ throw new InvalidArgumentException(
+ "Invalid static check script(s): " . implode(', ', $invalidCheckNames) . "."
+ );
+ }
+ }
}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php
index 94ff40069..d6e6b9a69 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php
@@ -6,23 +6,37 @@
namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers;
+use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector;
use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
+use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\BaseStorage;
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\FileStorage;
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\VaultStorage;
+use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\AwsSecretsManagerStorage;
use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter;
class CredentialStore
{
const ARRAY_KEY_FOR_VAULT = 'vault';
const ARRAY_KEY_FOR_FILE = 'file';
+ const ARRAY_KEY_FOR_AWS_SECRETS_MANAGER = 'aws';
+
+ const CREDENTIAL_STORAGE_INFO = 'You need to configure at least one of these options: '
+ . '.credentials file, HashiCorp Vault or AWS Secrets Manager correctly';
/**
* Credential storage array
*
- * @var array
+ * @var BaseStorage[]
*/
private $credStorage = [];
+ /**
+ * Boolean to indicate if credential storage have been initialized
+ *
+ * @var boolean
+ */
+ private $initialized;
+
/**
* Singleton instance
*
@@ -30,11 +44,17 @@ class CredentialStore
*/
private static $INSTANCE = null;
+ /**
+ * Exception contexts
+ *
+ * @var ExceptionCollector
+ */
+ private $exceptionContexts;
+
/**
* Static singleton getter for CredentialStore Instance
*
* @return CredentialStore
- * @throws TestFrameworkException
*/
public static function getInstance()
{
@@ -47,35 +67,11 @@ public static function getInstance()
/**
* CredentialStore constructor
- *
- * @throws TestFrameworkException
*/
private function __construct()
{
- // Initialize file storage
- try {
- $this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage();
- } catch (TestFrameworkException $e) {
- }
-
- // Initialize vault storage
- $cvAddress = getenv('CREDENTIAL_VAULT_ADDRESS');
- $cvSecretPath = getenv('CREDENTIAL_VAULT_SECRET_BASE_PATH');
- if ($cvAddress !== false && $cvSecretPath !== false) {
- try {
- $this->credStorage[self::ARRAY_KEY_FOR_VAULT] = new VaultStorage(
- UrlFormatter::format($cvAddress, false),
- '/' . trim($cvSecretPath, '/')
- );
- } catch (TestFrameworkException $e) {
- }
- }
-
- if (empty($this->credStorage)) {
- throw new TestFrameworkException(
- "No credential storage is properly configured. Please configure vault or .credentials file."
- );
- }
+ $this->initialized = false;
+ $this->exceptionContexts = new ExceptionCollector();
}
/**
@@ -87,8 +83,11 @@ private function __construct()
*/
public function getSecret($key)
{
- // Get secret data from storage according to the order they are stored
- // File storage is preferred over vault storage to allow local secret value overriding remote secret value
+ // Initialize credential storage if it's not been done
+ $this->initializeCredentialStorage();
+
+ // Get secret data from storage according to the order they are stored which follows this precedence:
+ // FileStorage > VaultStorage > AwsSecretsManagerStorage
foreach ($this->credStorage as $storage) {
$value = $storage->getEncryptedValue($key);
if (null !== $value) {
@@ -96,9 +95,12 @@ public function getSecret($key)
}
}
+ $exceptionContexts = $this->getExceptionContexts();
+ $this->resetExceptionContext();
throw new TestFrameworkException(
- "\"{$key}\" not defined in vault or .credentials file, "
- . "please provide a value in order to use this secret in a test."
+ "{$key} not found. " . self::CREDENTIAL_STORAGE_INFO
+ . " and ensure key, value exists to use _CREDS in tests."
+ . $exceptionContexts
);
}
@@ -106,27 +108,187 @@ public function getSecret($key)
* Return decrypted input value
*
* @param string $value
- * @return string
+ * @return string|false The decrypted string on success or false on failure
+ * @throws TestFrameworkException
*/
public function decryptSecretValue($value)
{
- // Loop through storage to decrypt value
- foreach ($this->credStorage as $storage) {
- return $storage->getDecryptedValue($value);
- }
+ // Initialize credential storage if it's not been done
+ $this->initializeCredentialStorage();
+
+ // Decrypt secret value
+ return BaseStorage::getDecryptedValue($value);
}
/**
* Return decrypted values for all occurrences from input string
*
* @param string $string
- * @return mixed
+ * @return string|false The decrypted string on success or false on failure
+ * @throws TestFrameworkException
*/
public function decryptAllSecretsInString($string)
{
- // Loop through storage to decrypt all occurrences from input string
- foreach ($this->credStorage as $storage) {
- return $storage->getAllDecryptedValuesInString($string);
+ // Initialize credential storage if it's not been done
+ $this->initializeCredentialStorage();
+
+ // Decrypt all secret values in string
+ return BaseStorage::getAllDecryptedValuesInString($string);
+ }
+
+ /**
+ * Setter for exception contexts
+ *
+ * @param string $type
+ * @param string $context
+ * @return void
+ */
+ public function setExceptionContexts($type, $context)
+ {
+ $typeArray = [self::ARRAY_KEY_FOR_FILE, self::ARRAY_KEY_FOR_VAULT, self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER];
+ if (in_array($type, $typeArray) && !empty($context)) {
+ $this->exceptionContexts->addError($type, $context);
+ }
+ }
+
+ /**
+ * Return collected exception contexts
+ *
+ * @return string
+ */
+ private function getExceptionContexts()
+ {
+ // Gather all exceptions collected
+ $exceptionMessage = "\n";
+ foreach ($this->exceptionContexts->getErrors() as $type => $exceptions) {
+ $exceptionMessage .= "\nException from ";
+ if ($type == self::ARRAY_KEY_FOR_FILE) {
+ $exceptionMessage .= "File Storage: \n";
+ }
+ if ($type == self::ARRAY_KEY_FOR_VAULT) {
+ $exceptionMessage .= "Vault Storage: \n";
+ }
+ if ($type == self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER) {
+ $exceptionMessage .= "AWS Secrets Manager Storage: \n";
+ }
+
+ if (is_array($exceptions)) {
+ $exceptionMessage .= implode("\n", $exceptions) . "\n";
+ } else {
+ $exceptionMessage .= $exceptions . "\n";
+ }
+ }
+ return $exceptionMessage;
+ }
+
+ /**
+ * Reset exception contexts to empty array
+ *
+ * @return void
+ */
+ private function resetExceptionContext()
+ {
+ $this->exceptionContexts->reset();
+ }
+
+ /**
+ * Initialize all available credential storage
+ *
+ * @return void
+ * @throws TestFrameworkException
+ */
+ private function initializeCredentialStorage()
+ {
+ if (!$this->initialized) {
+ // Initialize credential storage by defined order of precedence as the following
+ $this->initializeFileStorage();
+ $this->initializeVaultStorage();
+ $this->initializeAwsSecretsManagerStorage();
+ $this->initialized = true;
+ }
+
+ if (empty($this->credStorage)) {
+ throw new TestFrameworkException(
+ 'Invalid Credential Storage. ' . self::CREDENTIAL_STORAGE_INFO
+ . '.' . $this->getExceptionContexts()
+ );
+ }
+ $this->resetExceptionContext();
+ }
+
+ /**
+ * Initialize file storage
+ *
+ * @return void
+ */
+ private function initializeFileStorage()
+ {
+ // Initialize file storage
+ try {
+ $this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage();
+ } catch (TestFrameworkException $e) {
+ // Print error message in console
+ print_r($e->getMessage());
+ // Save to exception context for Allure report
+ $this->setExceptionContexts(self::ARRAY_KEY_FOR_FILE, $e->getMessage());
+ }
+ }
+
+ /**
+ * Initialize Vault storage
+ *
+ * @return void
+ */
+ private function initializeVaultStorage()
+ {
+ // Initialize vault storage
+ $cvAddress = getenv('CREDENTIAL_VAULT_ADDRESS');
+ $cvSecretPath = getenv('CREDENTIAL_VAULT_SECRET_BASE_PATH');
+ if ($cvAddress !== false && $cvSecretPath !== false) {
+ try {
+ $this->credStorage[self::ARRAY_KEY_FOR_VAULT] = new VaultStorage(
+ UrlFormatter::format($cvAddress, false),
+ '/' . trim($cvSecretPath, '/')
+ );
+ } catch (TestFrameworkException $e) {
+ // Print error message in console
+ print_r($e->getMessage());
+ // Save to exception context for Allure report
+ $this->setExceptionContexts(self::ARRAY_KEY_FOR_VAULT, $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * Initialize AWS Secrets Manager storage
+ *
+ * @return void
+ */
+ private function initializeAwsSecretsManagerStorage()
+ {
+ // Initialize AWS Secrets Manager storage
+ $awsRegion = getenv('CREDENTIAL_AWS_SECRETS_MANAGER_REGION');
+ $awsProfile = getenv('CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE');
+ $awsId = getenv('CREDENTIAL_AWS_ACCOUNT_ID');
+ if (!empty($awsRegion)) {
+ if (empty($awsProfile)) {
+ $awsProfile = null;
+ }
+ if (empty($awsId)) {
+ $awsId = null;
+ }
+ try {
+ $this->credStorage[self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER] = new AwsSecretsManagerStorage(
+ $awsRegion,
+ $awsProfile,
+ $awsId
+ );
+ } catch (TestFrameworkException $e) {
+ // Print error message in console
+ print_r($e->getMessage());
+ // Save to exception context for Allure report
+ $this->setExceptionContexts(self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER, $e->getMessage());
+ }
}
}
}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php
index 95a2b8b93..a210710db 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php
@@ -13,6 +13,7 @@
use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface;
use Magento\FunctionalTestingFramework\ObjectManagerFactory;
use Magento\FunctionalTestingFramework\DataGenerator\Util\DataExtensionUtil;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
class DataObjectHandler implements ObjectHandlerInterface
{
@@ -119,6 +120,7 @@ public function getAllObjects()
* @param string[] $parserOutput Primitive array output from the Magento parser.
* @return EntityDataObject[]
* @throws XmlException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function processParserOutput($parserOutput)
{
@@ -132,6 +134,7 @@ private function processParserOutput($parserOutput)
$type = $rawEntity[self::_TYPE] ?? null;
$data = [];
+ $deprecated = null;
$linkedEntities = [];
$uniquenessData = [];
$vars = [];
@@ -163,6 +166,14 @@ private function processParserOutput($parserOutput)
$parentEntity = $rawEntity[self::_EXTENDS];
}
+ if (array_key_exists(self::OBJ_DEPRECATED, $rawEntity)) {
+ $deprecated = $rawEntity[self::OBJ_DEPRECATED];
+ LoggingUtil::getInstance()->getLogger(self::class)->deprecation(
+ $deprecated,
+ ["dataName" => $filename, "deprecatedEntity" => $deprecated]
+ );
+ }
+
$entityDataObject = new EntityDataObject(
$name,
$type,
@@ -171,7 +182,8 @@ private function processParserOutput($parserOutput)
$uniquenessData,
$vars,
$parentEntity,
- $filename
+ $filename,
+ $deprecated
);
$entityDataObjects[$entityDataObject->getName()] = $entityDataObject;
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php
index b768d9bd8..8c2ec7b0a 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php
@@ -11,6 +11,7 @@
use Magento\FunctionalTestingFramework\DataGenerator\Util\OperationElementExtractor;
use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface;
use Magento\FunctionalTestingFramework\ObjectManagerFactory;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
class OperationDefinitionObjectHandler implements ObjectHandlerInterface
{
@@ -128,9 +129,7 @@ public function getOperationDefinition($operation, $dataType)
* @return void
* @throws \Exception
*
- * @SuppressWarnings(PHPMD.NPathComplexity)
- * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD)
*/
private function initialize()
{
@@ -145,6 +144,7 @@ private function initialize()
$auth = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH] ?? null;
$successRegex = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_SUCCESS_REGEX] ?? null;
$returnRegex = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_RETURN_REGEX] ?? null;
+ $deprecated = $opDefArray[ObjectHandlerInterface::OBJ_DEPRECATED] ?? null;
$returnIndex = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_RETURN_INDEX] ?? 0;
$contentType = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_CONTENT_TYPE][0]['value']
?? null;
@@ -205,6 +205,13 @@ private function initialize()
}
}
+ if ($deprecated !== null) {
+ LoggingUtil::getInstance()->getLogger(self::class)->deprecation(
+ $deprecated,
+ ["operationName" => $dataDefName, "deprecatedOperation" => $deprecated]
+ );
+ }
+
$this->operationDefinitionObjects[$operation . $dataType] = new OperationDefinitionObject(
$dataDefName,
$operation,
@@ -219,7 +226,8 @@ private function initialize()
$removeBackend,
$successRegex,
$returnRegex,
- $returnIndex
+ $returnIndex,
+ $deprecated
);
}
}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php
index 2a0f11a01..552b0dc07 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php
@@ -80,7 +80,7 @@ public function createEntity(
$entity,
$dependentObjectKeys = [],
$overrideFields = [],
- $storeCode = ""
+ $storeCode = "all"
) {
$retrievedDependentObjects = [];
foreach ($dependentObjectKeys as $objectKey) {
@@ -89,14 +89,24 @@ public function createEntity(
foreach ($overrideFields as $index => $field) {
try {
- $overrideFields[$index] = CredentialStore::getInstance()->decryptAllSecretsInString($field);
+ $decrptedField = CredentialStore::getInstance()->decryptAllSecretsInString($field);
+ if ($decrptedField !== false) {
+ $overrideFields[$index] = $decrptedField;
+ }
} catch (TestFrameworkException $e) {
- //do not rethrow if Credentials are not defined
- $overrideFields[$index] = $field;
+ //catch exception if Credentials are not defined
}
}
$retrievedEntity = DataObjectHandler::getInstance()->getObject($entity);
+
+ if ($retrievedEntity === null) {
+ throw new TestReferenceException(
+ "Entity \"" . $entity . "\" does not exist." .
+ "\nException occurred executing action at StepKey \"" . $key . "\""
+ );
+ }
+
$persistedObject = new DataPersistenceHandler(
$retrievedEntity,
$retrievedDependentObjects,
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorage.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorage.php
new file mode 100644
index 000000000..6bd1ff144
--- /dev/null
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorage.php
@@ -0,0 +1,212 @@
+createAwsSecretsManagerClient($region, $profile);
+ $this->region = $region;
+ $this->awsAccountId = $accountId;
+ }
+
+ /**
+ * Returns the value of a secret based on corresponding key
+ *
+ * @param string $key
+ * @return string|null
+ * @throws Exception
+ */
+ public function getEncryptedValue($key)
+ {
+ // Check if secret is in cached array
+ if (null !== ($value = parent::getEncryptedValue($key))) {
+ return $value;
+ }
+
+ if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
+ LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug(
+ "Retrieving value for key name {$key} from AWS Secrets Manager"
+ );
+ }
+
+ $reValue = null;
+ try {
+ // Split vendor/key to construct secret id
+ list($vendor, $key) = explode('/', trim($key, '/'), 2);
+ // If AWS account id is specified, create and use full ARN, otherwise use partial ARN as secret id
+ $secretId = '';
+ if (!empty($this->awsAccountId)) {
+ $secretId = self::AWS_SM_PARTIAL_ARN . $this->region . ':' . $this->awsAccountId . ':secret:';
+ }
+ $secretId .= self::MFTF_PATH
+ . '/'
+ . $vendor
+ . '/'
+ . $key;
+ // Read value by id from AWS Secrets Manager, and parse the result
+ $value = $this->parseAwsSecretResult(
+ $this->client->getSecretValue(['SecretId' => $secretId]),
+ $key
+ );
+ // Encrypt value for return
+ $reValue = openssl_encrypt($value, parent::ENCRYPTION_ALGO, parent::$encodedKey, 0, parent::$iv);
+ parent::$cachedSecretData[$key] = $reValue;
+ } catch (AwsException $e) {
+ $errMessage = "\nAWS Exception:\n" . $e->getAwsErrorMessage()
+ . "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
+ // Print error message in console
+ print_r($errMessage);
+ // Add error message in mftf log if verbose is enable
+ if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
+ LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
+ }
+ // Save to exception context for Allure report
+ CredentialStore::getInstance()->setExceptionContexts(
+ CredentialStore::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER,
+ $errMessage
+ );
+ } catch (\Exception $e) {
+ $errMessage = "\nException:\n" . $e->getMessage()
+ . "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
+ // Print error message in console
+ print_r($errMessage);
+ // Add error message in mftf log if verbose is enable
+ if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
+ LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
+ }
+ // Save to exception context for Allure report
+ CredentialStore::getInstance()->setExceptionContexts(
+ CredentialStore::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER,
+ $errMessage
+ );
+ }
+ return $reValue;
+ }
+
+ /**
+ * Parse AWS result object and return secret for key
+ *
+ * @param Result $awsResult
+ * @param string $key
+ * @return string
+ * @throws TestFrameworkException
+ */
+ private function parseAwsSecretResult($awsResult, $key)
+ {
+ // Return secret from the associated KMS CMK
+ if (isset($awsResult['SecretString'])) {
+ $rawSecret = $awsResult['SecretString'];
+ } else {
+ throw new TestFrameworkException(
+ "'SecretString' field is not set in AWS Result. Error parsing result from AWS Secrets Manager"
+ );
+ }
+
+ // Secrets are saved as JSON structures of key/value pairs if using AWS Secrets Manager console, and
+ // Secrets are saved as plain text if using AWS CLI. We need to handle both cases.
+ $secret = json_decode($rawSecret, true);
+ if (isset($secret[$key])) {
+ return $secret[$key];
+ } elseif (is_string($rawSecret)) {
+ return $rawSecret;
+ }
+ throw new TestFrameworkException(
+ "$key not found or value is not string . Error parsing result from AWS Secrets Manager"
+ );
+ }
+
+ /**
+ * Create Aws Secrets Manager client
+ *
+ * @param string $region
+ * @param string $profile
+ * @return void
+ * @throws TestFrameworkException
+ * @throws InvalidArgumentException
+ */
+ private function createAwsSecretsManagerClient($region, $profile)
+ {
+ if (null !== $this->client) {
+ return;
+ }
+
+ $options = [
+ 'region' => $region,
+ 'version' => self::LATEST_VERSION,
+ ];
+
+ if (!empty($profile)) {
+ $options['profile'] = $profile;
+ }
+
+ // Create AWS Secrets Manager client
+ $this->client = new SecretsManagerClient($options);
+ if ($this->client === null) {
+ throw new TestFrameworkException("Unable to create AWS Secrets Manager client");
+ }
+ }
+}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/BaseStorage.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/BaseStorage.php
index cb892a545..6eb7fa7e8 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/BaseStorage.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/BaseStorage.php
@@ -58,30 +58,38 @@ public function getEncryptedValue($key)
/**
* Takes a value encrypted at runtime and decrypts it using the object's initial vector
+ * return the decrypted string on success or false on failure
*
* @param string $value
- * @return string
+ * @return string|false The decrypted string on success or false on failure
*/
- public function getDecryptedValue($value)
+ public static function getDecryptedValue($value)
{
return openssl_decrypt($value, self::ENCRYPTION_ALGO, self::$encodedKey, 0, self::$iv);
}
/**
* Takes a string that contains encrypted data at runtime and decrypts each value
+ * return false if no decryption happens or a failure occurs
*
* @param string $string
- * @return mixed
+ * @return string|false The decrypted string on success or false on failure
*/
- public function getAllDecryptedValuesInString($string)
+ public static function getAllDecryptedValuesInString($string)
{
- $newString = $string;
+ $decrypted = false;
foreach (self::$cachedSecretData as $key => $secretValue) {
- if (strpos($newString, $secretValue) !== false) {
+ if (strpos($string, $secretValue) !== false) {
$decryptedValue = self::getDecryptedValue($secretValue);
- $newString = str_replace($secretValue, $decryptedValue, $newString);
+ if ($decryptedValue === false) {
+ return false;
+ }
+ if (!$decrypted) {
+ $decrypted = true;
+ }
+ $string = str_replace($secretValue, $decryptedValue, $string);
}
}
- return $newString;
+ return $decrypted ? $string : false;
}
}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php
index 6a9e9f0cf..39839e8f9 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php
@@ -7,6 +7,7 @@
namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage;
use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig;
+use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
use Vault\Client;
@@ -67,7 +68,7 @@ class VaultStorage extends BaseStorage
private $secretBasePath;
/**
- * CredentialVault constructor
+ * VaultStorage constructor
*
* @param string $baseUrl
* @param string $secretBasePath
@@ -123,10 +124,14 @@ public function getEncryptedValue($key)
$reValue = openssl_encrypt($value, parent::ENCRYPTION_ALGO, parent::$encodedKey, 0, parent::$iv);
parent::$cachedSecretData[$key] = $reValue;
} catch (\Exception $e) {
+ $errMessage = "\nUnable to read secret for key name {$key} from vault." . $e->getMessage();
+ // Print error message in console
+ print_r($errMessage);
+ // Save to exception context for Allure report
+ CredentialStore::getInstance()->setExceptionContexts('vault', $errMessage);
+ // Add error message in mftf log if verbose is enable
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
- LoggingUtil::getInstance()->getLogger(VaultStorage::class)->debug(
- "Unable to read secret for key name {$key} from vault"
- );
+ LoggingUtil::getInstance()->getLogger(VaultStorage::class)->debug($errMessage);
}
}
return $reValue;
@@ -149,6 +154,13 @@ private function authenticated()
return true;
}
} catch (\Exception $e) {
+ // Print error message in console
+ print_r($e->getMessage());
+ // Save to exception context for Allure report
+ CredentialStore::getInstance()->setExceptionContexts(
+ CredentialStore::ARRAY_KEY_FOR_VAULT,
+ $e->getMessage()
+ );
}
return false;
}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php
index 47a966f84..2204364f5 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php
@@ -80,17 +80,25 @@ class EntityDataObject
*/
private $filename;
+ /**
+ * Deprecated message.
+ *
+ * @var string
+ */
+ private $deprecated;
+
/**
* Constructor
*
- * @param string $name
- * @param string $type
- * @param string[] $data
- * @param string[] $linkedEntities
- * @param string[] $uniquenessData
- * @param string[] $vars
- * @param string $parentEntity
- * @param string $filename
+ * @param string $name
+ * @param string $type
+ * @param string[] $data
+ * @param string[] $linkedEntities
+ * @param string[] $uniquenessData
+ * @param string[] $vars
+ * @param string $parentEntity
+ * @param string $filename
+ * @param string|null $deprecated
*/
public function __construct(
$name,
@@ -100,7 +108,8 @@ public function __construct(
$uniquenessData,
$vars = [],
$parentEntity = null,
- $filename = null
+ $filename = null,
+ $deprecated = null
) {
$this->name = $name;
$this->type = $type;
@@ -113,6 +122,17 @@ public function __construct(
$this->vars = $vars;
$this->parentEntity = $parentEntity;
$this->filename = $filename;
+ $this->deprecated = $deprecated;
+ }
+
+ /**
+ * Getter for the deprecated attr of the section.
+ *
+ * @return string
+ */
+ public function getDeprecated()
+ {
+ return $this->deprecated;
}
/**
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php
index 721a9dcc9..67fe48300 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php
@@ -8,6 +8,7 @@
/**
* Class OperationDefinitionObject
+ * @SuppressWarnings(PHPMD)
*/
class OperationDefinitionObject
{
@@ -117,22 +118,30 @@ class OperationDefinitionObject
*/
private $removeBackend;
+ /**
+ * Deprecated message.
+ *
+ * @var string
+ */
+ private $deprecated;
+
/**
* OperationDefinitionObject constructor.
- * @param string $name
- * @param string $operation
- * @param string $dataType
- * @param string $apiMethod
- * @param string $apiUri
- * @param string $auth
- * @param array $headers
- * @param array $params
- * @param array $metaData
- * @param string $contentType
- * @param boolean $removeBackend
- * @param string $successRegex
- * @param string $returnRegex
- * @param string $returnIndex
+ * @param string $name
+ * @param string $operation
+ * @param string $dataType
+ * @param string $apiMethod
+ * @param string $apiUri
+ * @param string $auth
+ * @param array $headers
+ * @param array $params
+ * @param array $metaData
+ * @param string $contentType
+ * @param boolean $removeBackend
+ * @param string $successRegex
+ * @param string $returnRegex
+ * @param string $returnIndex
+ * @param string|null $deprecated
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -149,7 +158,8 @@ public function __construct(
$removeBackend,
$successRegex = null,
$returnRegex = null,
- $returnIndex = null
+ $returnIndex = null,
+ $deprecated = null
) {
$this->name = $name;
$this->operation = $operation;
@@ -164,6 +174,7 @@ public function __construct(
$this->returnRegex = $returnRegex;
$this->returnIndex = $returnIndex;
$this->removeBackend = $removeBackend;
+ $this->deprecated = $deprecated;
$this->apiUrl = null;
if (!empty($contentType)) {
@@ -176,6 +187,16 @@ public function __construct(
$this->headers[] = self::HTTP_CONTENT_TYPE_HEADER . ': ' . $this->contentType;
}
+ /**
+ * Getter for the deprecated attr of the section
+ *
+ * @return string
+ */
+ public function getDeprecated()
+ {
+ return $this->deprecated;
+ }
+
/**
* Getter for data's data type
*
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php
index dcb9d158a..73ee2be19 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php
@@ -59,7 +59,10 @@ public function __construct($entityObject, $dependentObjects = [], $customFields
array_merge($entityObject->getAllData(), $customFields),
$entityObject->getLinkedEntities(),
$this->stripCustomFieldsFromUniquenessData($entityObject->getUniquenessData(), $customFields),
- $entityObject->getVarReferences()
+ $entityObject->getVarReferences(),
+ $entityObject->getParentName(),
+ $entityObject->getFilename(),
+ $entityObject->getDeprecated()
);
} else {
$this->entityObject = clone $entityObject;
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php
index 2766b39a0..3a12bdce1 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php
@@ -93,7 +93,8 @@ public function extendEntity($entityObject)
$newUniqueReferences,
$newVarReferences,
$entityObject->getParentName(),
- $entityObject->getFilename()
+ $entityObject->getFilename(),
+ $entityObject->getDeprecated()
);
return $extendedEntity;
}
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/RuntimeDataReferenceResolver.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/RuntimeDataReferenceResolver.php
index a09afd01a..220d4ab7f 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/RuntimeDataReferenceResolver.php
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/RuntimeDataReferenceResolver.php
@@ -9,6 +9,7 @@
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler;
use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException;
+use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
use Magento\FunctionalTestingFramework\Test\Objects\ActionObject;
/**
@@ -23,7 +24,7 @@ class RuntimeDataReferenceResolver implements DataReferenceResolverInterface
* @param string $originalDataEntity
* @return array|false|string|null
* @throws TestReferenceException
- * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException
+ * @throws TestFrameworkException
*/
public function getDataReference(string $data, string $originalDataEntity)
{
@@ -43,6 +44,9 @@ public function getDataReference(string $data, string $originalDataEntity)
case ActionObject::__CREDS:
$value = CredentialStore::getInstance()->getSecret($var);
$result = CredentialStore::getInstance()->decryptSecretValue($value);
+ if ($result === false) {
+ throw new TestFrameworkException("\nFailed to decrypt value {$value}\n");
+ }
$result = str_replace($matches['reference'], $result, $data);
break;
default:
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd
index 8b59ccae0..58fad8d8b 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd
@@ -22,6 +22,13 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd
index aec157309..9d0d8bb35 100644
--- a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd
+++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd
@@ -69,6 +69,14 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php b/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php
index c1e023ef4..f64b35446 100644
--- a/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php
+++ b/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php
@@ -43,6 +43,26 @@ public function throwException()
throw new \Exception("\n" . $errorMsg);
}
+ /**
+ * Return all errors
+ *
+ * @return array
+ */
+ public function getErrors()
+ {
+ return $this->errors ?? [];
+ }
+
+ /**
+ * Reset error to empty array
+ *
+ * @return void
+ */
+ public function reset()
+ {
+ $this->errors = [];
+ }
+
/**
* If there are multiple exceptions for a single file, the function flattens the array so they can be printed
* as separate messages.
diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php
index a95586b26..cd2768a06 100644
--- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php
+++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php
@@ -53,6 +53,9 @@ class MagentoWebDriver extends WebDriver
{
use AttachmentSupport;
+ const MAGENTO_CRON_INTERVAL = 60;
+ const MAGENTO_CRON_COMMAND = 'cron:run';
+
/**
* List of known magento loading masks by selector
*
@@ -121,6 +124,13 @@ class MagentoWebDriver extends WebDriver
*/
private $jsErrors = [];
+ /**
+ * Contains last execution times for Cron
+ *
+ * @var int[]
+ */
+ private $cronExecution = [];
+
/**
* Sanitizes config, then initializes using parent.
*
@@ -552,6 +562,75 @@ public function magentoCLI($command, $timeout = null, $arguments = null)
return $response;
}
+ /**
+ * Executes Magento Cron keeping the interval (> 60 seconds between each run)
+ *
+ * @param string|null $cronGroups
+ * @param integer|null $timeout
+ * @param string|null $arguments
+ * @return string
+ */
+ public function magentoCron($cronGroups = null, $timeout = null, $arguments = null)
+ {
+ $cronGroups = explode(' ', $cronGroups);
+ return $this->executeCronjobs($cronGroups, $timeout, $arguments);
+ }
+
+ /**
+ * Updates last execution time for Cron
+ *
+ * @param array $cronGroups
+ * @return void
+ */
+ private function notifyCronFinished(array $cronGroups = [])
+ {
+ if (empty($cronGroups)) {
+ $this->cronExecution['*'] = time();
+ }
+
+ foreach ($cronGroups as $group) {
+ $this->cronExecution[$group] = time();
+ }
+ }
+
+ /**
+ * Returns last Cron execution time for specific cron or all crons
+ *
+ * @param array $cronGroups
+ * @return integer
+ */
+ private function getLastCronExecution(array $cronGroups = [])
+ {
+ if (empty($cronGroups)) {
+ return (int)max($this->cronExecution);
+ }
+
+ $cronGroups = array_merge($cronGroups, ['*']);
+
+ return array_reduce($cronGroups, function ($lastExecution, $group) {
+ if (isset($this->cronExecution[$group]) && $this->cronExecution[$group] > $lastExecution) {
+ $lastExecution = $this->cronExecution[$group];
+ }
+
+ return (int)$lastExecution;
+ }, 0);
+ }
+
+ /**
+ * Returns time to wait for next run
+ *
+ * @param array $cronGroups
+ * @param integer $cronInterval
+ * @return integer
+ */
+ private function getCronWait(array $cronGroups = [], int $cronInterval = self::MAGENTO_CRON_INTERVAL)
+ {
+ $nextRun = $this->getLastCronExecution($cronGroups) + $cronInterval;
+ $toNextRun = $nextRun - time();
+
+ return max(0, $toNextRun);
+ }
+
/**
* Runs DELETE request to delete a Magento entity against the url given.
*
@@ -690,6 +769,9 @@ public function fillSecretField($field, $value)
// decrypted value
$decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value);
+ if ($decryptedValue === false) {
+ throw new TestFrameworkException("\nFailed to decrypt value {$value} for field {$field}\n");
+ }
$this->fillField($field, $decryptedValue);
}
@@ -709,6 +791,9 @@ public function magentoCLISecret($command, $timeout = null, $arguments = null)
// decrypted value
$decryptedCommand = CredentialStore::getInstance()->decryptAllSecretsInString($command);
+ if ($decryptedCommand === false) {
+ throw new TestFrameworkException("\nFailed to decrypt magentoCLI command {$command}\n");
+ }
return $this->magentoCLI($decryptedCommand, $timeout, $arguments);
}
@@ -971,4 +1056,36 @@ public function getSecret($key)
{
return CredentialStore::getInstance()->getSecret($key);
}
+
+ /**
+ * Waits proper amount of time to perform Cron execution
+ *
+ * @param string $cronGroups
+ * @param integer $timeout
+ * @param string $arguments
+ * @return string
+ * @throws TestFrameworkException
+ */
+ private function executeCronjobs($cronGroups, $timeout, $arguments): string
+ {
+ $cronGroups = array_filter($cronGroups);
+
+ $waitFor = $this->getCronWait($cronGroups);
+
+ if ($waitFor) {
+ $this->wait($waitFor);
+ }
+
+ $command = array_reduce($cronGroups, function ($command, $cronGroup) {
+ $command .= ' --group=' . $cronGroup;
+ return $command;
+ }, self::MAGENTO_CRON_COMMAND);
+ $timeStart = microtime(true);
+ $cronResult = $this->magentoCLI($command, $timeout, $arguments);
+ $timeEnd = microtime(true);
+
+ $this->notifyCronFinished($cronGroups);
+
+ return sprintf('%s (wait: %ss, execution: %ss)', $cronResult, $waitFor, round($timeEnd - $timeStart, 2));
+ }
}
diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php
index 1bc79cbbb..94147dc08 100644
--- a/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php
+++ b/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php
@@ -11,6 +11,8 @@
*/
interface ObjectHandlerInterface
{
+ const OBJ_DEPRECATED = 'deprecated';
+
/**
* Function to enforce singleton design pattern
*
diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php
index d9eec1af1..c310b9bef 100644
--- a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php
+++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php
@@ -9,6 +9,7 @@
use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface;
use Magento\FunctionalTestingFramework\ObjectManagerFactory;
use Magento\FunctionalTestingFramework\Page\Objects\PageObject;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
use Magento\FunctionalTestingFramework\XmlParser\PageParser;
use Magento\FunctionalTestingFramework\Exceptions\XmlException;
@@ -68,9 +69,17 @@ private function __construct()
$sectionNames = array_keys($pageData[self::SECTION] ?? []);
$parameterized = $pageData[self::PARAMETERIZED] ?? false;
$filename = $pageData[self::FILENAME] ?? null;
+ $deprecated = $pageData[self::OBJ_DEPRECATED] ?? null;
+
+ if ($deprecated !== null) {
+ LoggingUtil::getInstance()->getLogger(self::class)->deprecation(
+ $deprecated,
+ ["pageName" => $filename, "deprecatedPage" => $deprecated]
+ );
+ }
$this->pageObjects[$pageName] =
- new PageObject($pageName, $url, $module, $sectionNames, $parameterized, $area, $filename);
+ new PageObject($pageName, $url, $module, $sectionNames, $parameterized, $area, $filename, $deprecated);
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php
index e77e4c502..2736100fd 100644
--- a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php
+++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php
@@ -10,6 +10,8 @@
use Magento\FunctionalTestingFramework\ObjectManagerFactory;
use Magento\FunctionalTestingFramework\Page\Objects\ElementObject;
use Magento\FunctionalTestingFramework\Page\Objects\SectionObject;
+use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
use Magento\FunctionalTestingFramework\XmlParser\SectionParser;
use Magento\FunctionalTestingFramework\Exceptions\XmlException;
@@ -73,14 +75,21 @@ private function __construct()
$elementLocatorFunc = $elementData[SectionObjectHandler::LOCATOR_FUNCTION] ?? null;
$elementTimeout = $elementData[SectionObjectHandler::TIMEOUT] ?? null;
$elementParameterized = $elementData[SectionObjectHandler::PARAMETERIZED] ?? false;
-
+ $elementDeprecated = $elementData[self::OBJ_DEPRECATED] ?? null;
+ if ($elementDeprecated !== null) {
+ LoggingUtil::getInstance()->getLogger(ElementObject::class)->deprecation(
+ $elementDeprecated,
+ ["elementName" => $elementName, "deprecatedElement" => $elementDeprecated]
+ );
+ }
$elements[$elementName] = new ElementObject(
$elementName,
$elementType,
$elementSelector,
$elementLocatorFunc,
$elementTimeout,
- $elementParameterized
+ $elementParameterized,
+ $elementDeprecated
);
}
} catch (XmlException $exception) {
@@ -88,7 +97,21 @@ private function __construct()
}
$filename = $sectionData[self::FILENAME] ?? null;
- $this->sectionObjects[$sectionName] = new SectionObject($sectionName, $elements, $filename);
+ $sectionDeprecated = $sectionData[self::OBJ_DEPRECATED] ?? null;
+
+ if ($sectionDeprecated !== null) {
+ LoggingUtil::getInstance()->getLogger(SectionObject::class)->deprecation(
+ $sectionDeprecated,
+ ["sectionName" => $filename, "deprecatedSection" => $sectionDeprecated]
+ );
+ }
+
+ $this->sectionObjects[$sectionName] = new SectionObject(
+ $sectionName,
+ $elements,
+ $filename,
+ $sectionDeprecated
+ );
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php
index 6b0626f10..567872fc7 100644
--- a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php
+++ b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php
@@ -56,6 +56,13 @@ class ElementObject
*/
private $parameterized;
+ /**
+ * Deprecated message.
+ *
+ * @var string
+ */
+ private $deprecated;
+
/**
* ElementObject constructor.
* @param string $name
@@ -66,7 +73,7 @@ class ElementObject
* @param boolean $parameterized
* @throws XmlException
*/
- public function __construct($name, $type, $selector, $locatorFunction, $timeout, $parameterized)
+ public function __construct($name, $type, $selector, $locatorFunction, $timeout, $parameterized, $deprecated = null)
{
if ($selector != null && $locatorFunction != null) {
throw new XmlException("Element '{$name}' cannot have both a selector and a locatorFunction.");
@@ -83,6 +90,17 @@ public function __construct($name, $type, $selector, $locatorFunction, $timeout,
}
$this->timeout = $timeout;
$this->parameterized = $parameterized;
+ $this->deprecated = $deprecated;
+ }
+
+ /**
+ * Getter for the deprecated attr
+ *
+ * @return string
+ */
+ public function getDeprecated()
+ {
+ return $this->deprecated;
}
/**
diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php
index d99c40240..e4a1acdc0 100644
--- a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php
+++ b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php
@@ -65,18 +65,34 @@ class PageObject
*/
private $filename;
+ /**
+ * Deprecated message.
+ *
+ * @var string
+ */
+ private $deprecated;
+
/**
* PageObject constructor.
- * @param string $name
- * @param string $url
- * @param string $module
- * @param array $sections
- * @param boolean $parameterized
- * @param string $area
- * @param string $filename
- */
- public function __construct($name, $url, $module, $sections, $parameterized, $area, $filename = null)
- {
+ * @param string $name
+ * @param string $url
+ * @param string $module
+ * @param array $sections
+ * @param boolean $parameterized
+ * @param string $area
+ * @param string|null $filename
+ * @param string|null $deprecated
+ */
+ public function __construct(
+ $name,
+ $url,
+ $module,
+ $sections,
+ $parameterized,
+ $area,
+ $filename = null,
+ $deprecated = null
+ ) {
$this->name = $name;
$this->url = $url;
$this->module = $module;
@@ -84,6 +100,17 @@ public function __construct($name, $url, $module, $sections, $parameterized, $ar
$this->parameterized = $parameterized;
$this->area = $area;
$this->filename = $filename;
+ $this->deprecated = $deprecated;
+ }
+
+ /**
+ * Getter for the deprecated attr of the section
+ *
+ * @return string
+ */
+ public function getDeprecated()
+ {
+ return $this->deprecated;
}
/**
diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php
index 8c1cac326..3674eea16 100644
--- a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php
+++ b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php
@@ -32,17 +32,26 @@ class SectionObject
*/
private $filename;
+ /**
+ * Deprecated message.
+ *
+ * @var string
+ */
+ private $deprecated;
+
/**
* SectionObject constructor.
- * @param string $name
- * @param array $elements
- * @param string $filename
+ * @param string $name
+ * @param array $elements
+ * @param string|null $filename
+ * @param string|null $deprecated
*/
- public function __construct($name, $elements, $filename = null)
+ public function __construct($name, $elements, $filename = null, $deprecated = null)
{
$this->name = $name;
$this->elements = $elements;
$this->filename = $filename;
+ $this->deprecated = $deprecated;
}
/**
@@ -55,6 +64,16 @@ public function getName()
return $this->name;
}
+ /**
+ * Getter for the deprecated attr of the section
+ *
+ * @return string
+ */
+ public function getDeprecated()
+ {
+ return $this->deprecated;
+ }
+
/**
* Getter for the Section Filename
*
diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd
index c6f399a67..65ac8290d 100644
--- a/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd
+++ b/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd
@@ -48,6 +48,14 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd
index 44e057369..1b5b44ddd 100644
--- a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd
+++ b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd
@@ -44,6 +44,13 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
@@ -58,6 +65,13 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php
new file mode 100644
index 000000000..32c7661fe
--- /dev/null
+++ b/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php
@@ -0,0 +1,211 @@
+).)*/mxs';
+ const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/]*name="([^"\']*)/mxs';
+ const ACTIONGROUP_NAME_REGEX_PATTERN = '/getModulesPath();
+
+ $actionGroupXmlFiles = StaticCheckHelper::buildFileList(
+ $allModules,
+ DIRECTORY_SEPARATOR . 'ActionGroup' . DIRECTORY_SEPARATOR
+ );
+
+ $this->errors = $this->findErrorsInFileSet($actionGroupXmlFiles);
+
+ $this->output = StaticCheckHelper::printErrorsToFile(
+ $this->errors,
+ self::ERROR_LOG_FILENAME,
+ self::ERROR_LOG_MESSAGE
+ );
+ }
+
+ /**
+ * Return array containing all errors found after running the execute() function.
+ * @return array
+ */
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * Return string of a short human readable result of the check. For example: "No unused arguments found."
+ * @return string
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+ /**
+ * Finds all unused arguments in given set of actionGroup files
+ * @param Finder $files
+ * @return array $testErrors
+ */
+ private function findErrorsInFileSet($files)
+ {
+ $actionGroupErrors = [];
+ foreach ($files as $filePath) {
+ $contents = file_get_contents($filePath);
+ preg_match_all(self::ACTIONGROUP_XML_REGEX_PATTERN, $contents, $actionGroups);
+ $actionGroupToArguments = $this->buildUnusedArgumentList($actionGroups[0]);
+ $actionGroupErrors += $this->setErrorOutput($actionGroupToArguments, $filePath);
+ }
+ return $actionGroupErrors;
+ }
+
+ /**
+ * Builds array of action group => unused arguments
+ * @param array $actionGroups
+ * @return array $actionGroupToArguments
+ */
+ private function buildUnusedArgumentList($actionGroups)
+ {
+ $actionGroupToArguments = [];
+
+ foreach ($actionGroups as $actionGroupXml) {
+ preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $actionGroupXml, $actionGroupName);
+ $unusedArguments = $this->findUnusedArguments($actionGroupXml);
+ if (!empty($unusedArguments)) {
+ $actionGroupToArguments[$actionGroupName[1]] = $unusedArguments;
+ }
+ }
+ return $actionGroupToArguments;
+ }
+
+ /**
+ * Returns unused arguments in an action group
+ * @param string $actionGroupXml
+ * @return array
+ */
+ private function findUnusedArguments($actionGroupXml)
+ {
+ $unusedArguments = [];
+
+ preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $actionGroupXml, $arguments);
+ preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $actionGroupXml, $actionGroupName);
+ try {
+ $actionGroup = ActionGroupObjectHandler::getInstance()->getObject($actionGroupName[1]);
+ } catch (XmlException $e) {
+ }
+ foreach ($arguments[1] as $argument) {
+ //pattern to match all argument references
+ $patterns = [
+ '(\{{2}' . $argument . '(\.[a-zA-Z0-9_\[\]\(\).,\'\/ ]+)?}{2})',
+ '([(,\s\'$$]' . $argument . '(\.[a-zA-Z0-9_$\[\]]+)?[),\s\'])'
+ ];
+ // matches entity references
+ if (preg_match($patterns[0], $actionGroupXml)) {
+ continue;
+ }
+ //matches parametrized references
+ if (preg_match($patterns[1], $actionGroupXml)) {
+ continue;
+ }
+ //for extending action groups, exclude arguments that are also defined in parent action group
+ if ($this->isParentActionGroupArgument($argument, $actionGroup)) {
+ continue;
+ }
+ $unusedArguments[] = $argument;
+ }
+ return $unusedArguments;
+ }
+
+ /**
+ * Checks if the argument is also defined in the parent for extending action groups.
+ * @param string $argument
+ * @param ActionGroupObject $actionGroup
+ * @return boolean
+ */
+ private function isParentActionGroupArgument($argument, $actionGroup)
+ {
+ $parentActionGroupName = $actionGroup->getParentName();
+ if ($parentActionGroupName !== null) {
+ $parentActionGroup = ActionGroupObjectHandler::getInstance()->getObject($parentActionGroupName);
+ $parentArguments = $parentActionGroup->getArguments();
+ foreach ($parentArguments as $parentArgument) {
+ if ($argument === $parentArgument->getName()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Builds and returns error output for violating references
+ *
+ * @param array $actionGroupToArguments
+ * @param string $path
+ * @return mixed
+ */
+ private function setErrorOutput($actionGroupToArguments, $path)
+ {
+ $actionGroupErrors = [];
+ if (!empty($actionGroupToArguments)) {
+ // Build error output
+ $errorOutput = "\nFile \"{$path->getRealPath()}\"";
+ $errorOutput .= "\ncontains action group(s) with unused arguments.\n\t\t";
+ foreach ($actionGroupToArguments as $actionGroup => $arguments) {
+ $errorOutput .= "\n\t {$actionGroup} has unused argument(s): " . implode(", ", $arguments);
+ }
+ $actionGroupErrors[$path->getRealPath()][] = $errorOutput;
+ }
+ return $actionGroupErrors;
+ }
+}
diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckHelper.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckHelper.php
new file mode 100644
index 000000000..f534544fc
--- /dev/null
+++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckHelper.php
@@ -0,0 +1,56 @@
+ $error) {
+ fwrite($fileResource, $error[0] . PHP_EOL);
+ }
+
+ fclose($fileResource);
+ $errorCount = count($errors);
+ $output = $message . ": Errors found across {$errorCount} file(s). Error details output to {$outputPath}";
+
+ return $output;
+ }
+
+ /**
+ * Builds list of all XML files in given modulePaths + path given
+ * @param array $modulePaths
+ * @param string $path
+ * @return Finder
+ */
+ public static function buildFileList($modulePaths, $path)
+ {
+ $finder = new Finder();
+ foreach ($modulePaths as $modulePath) {
+ if (!realpath($modulePath . $path)) {
+ continue;
+ }
+ $finder->files()->in($modulePath . $path)->name("*.xml");
+ }
+ return $finder->files();
+ }
+}
diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php
index f3cf20739..ffa63389d 100644
--- a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php
+++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php
@@ -29,6 +29,7 @@ public function __construct(array $checks = [])
{
$this->checks = [
'testDependencies' => new TestDependencyCheck(),
+ 'actionGroupArguments' => new ActionGroupArgumentsCheck(),
] + $checks;
}
diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php
index 19725dc18..3c3ed6a37 100644
--- a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php
+++ b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php
@@ -32,6 +32,9 @@ class TestDependencyCheck implements StaticCheckInterface
const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/';
const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/]*name="([^"\']*)/';
+ const ERROR_LOG_FILENAME = 'mftf-dependency-checks';
+ const ERROR_LOG_MESSAGE = 'MFTF File Dependency Check';
+
/**
* Array of FullModuleName => [dependencies]
* @var array
@@ -113,9 +116,9 @@ public function execute(InputInterface $input)
DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR,
];
// These files can contain references to other modules.
- $testXmlFiles = $this->buildFileList($allModules, $filePaths[0]);
- $actionGroupXmlFiles = $this->buildFileList($allModules, $filePaths[1]);
- $dataXmlFiles= $this->buildFileList($allModules, $filePaths[2]);
+ $testXmlFiles = StaticCheckHelper::buildFileList($allModules, $filePaths[0]);
+ $actionGroupXmlFiles = StaticCheckHelper::buildFileList($allModules, $filePaths[1]);
+ $dataXmlFiles= StaticCheckHelper::buildFileList($allModules, $filePaths[2]);
$this->errors = [];
$this->errors += $this->findErrorsInFileSet($testXmlFiles);
@@ -123,7 +126,11 @@ public function execute(InputInterface $input)
$this->errors += $this->findErrorsInFileSet($dataXmlFiles);
// hold on to the output and print any errors to a file
- $this->output = $this->printErrorsToFile();
+ $this->output = StaticCheckHelper::printErrorsToFile(
+ $this->errors,
+ self::ERROR_LOG_FILENAME,
+ self::ERROR_LOG_MESSAGE
+ );
}
/**
@@ -413,24 +420,6 @@ private function getModuleDependenciesFromReferences($array)
return $filenames;
}
- /**
- * Builds list of all XML files in given modulePaths + path given
- * @param string $modulePaths
- * @param string $path
- * @return Finder
- */
- private function buildFileList($modulePaths, $path)
- {
- $finder = new Finder();
- foreach ($modulePaths as $modulePath) {
- if (!realpath($modulePath . $path)) {
- continue;
- }
- $finder->files()->in($modulePath . $path)->name("*.xml");
- }
- return $finder->files();
- }
-
/**
* Attempts to find any MFTF entity by its name. Returns null if none are found.
* @param string $name
@@ -461,32 +450,4 @@ private function findEntity($name)
}
return null;
}
-
- /**
- * Prints out given errors to file, and returns summary result string
- * @return string
- */
- private function printErrorsToFile()
- {
- $errors = $this->getErrors();
-
- if (empty($errors)) {
- return "No Dependency errors found.";
- }
-
- $outputPath = getcwd() . DIRECTORY_SEPARATOR . "mftf-dependency-checks.txt";
- $fileResource = fopen($outputPath, 'w');
- $header = "MFTF File Dependency Check:\n";
- fwrite($fileResource, $header);
-
- foreach ($errors as $test => $error) {
- fwrite($fileResource, $error[0] . PHP_EOL);
- }
-
- fclose($fileResource);
- $errorCount = count($errors);
- $output = "Dependency errors found across {$errorCount} file(s). Error details output to {$outputPath}";
-
- return $output;
- }
}
diff --git a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php
index 30a67c31d..bbaaf4379 100644
--- a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php
+++ b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php
@@ -5,6 +5,7 @@
*/
namespace Magento\FunctionalTestingFramework\Suite\Handlers;
+use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException;
use Magento\FunctionalTestingFramework\Exceptions\XmlException;
use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface;
use Magento\FunctionalTestingFramework\ObjectManagerFactory;
@@ -73,7 +74,7 @@ public static function getInstance(): ObjectHandlerInterface
public function getObject($objectName): SuiteObject
{
if (!array_key_exists($objectName, $this->suiteObjects)) {
- trigger_error("Suite ${objectName} is not defined.", E_USER_ERROR);
+ throw new TestReferenceException("Suite ${objectName} is not defined in xml.");
}
return $this->suiteObjects[$objectName];
}
diff --git a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php
index 4df7daa75..e3da10497 100644
--- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php
+++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php
@@ -178,12 +178,14 @@ private function validateTestsReferencedInSuite($suiteName, $testsReferenced, $o
{
$suiteRef = $originalSuiteName ?? $suiteName;
$possibleTestRef = SuiteObjectHandler::getInstance()->getObject($suiteRef)->getTests();
- $errorMsg = "Cannot reference tests whcih are not declared as part of suite.";
+ $errorMsg = "Cannot reference tests which are not declared as part of suite";
$invalidTestRef = array_diff($testsReferenced, array_keys($possibleTestRef));
if (!empty($invalidTestRef)) {
- throw new TestReferenceException($errorMsg, ['suite' => $suiteRef, 'test' => $invalidTestRef]);
+ $testList = implode("\", \"", $invalidTestRef);
+ $fullError = $errorMsg . " (Suite: \"{$suiteRef}\" Tests: \"{$testList}\")";
+ throw new TestReferenceException($fullError, ['suite' => $suiteRef, 'test' => $invalidTestRef]);
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd b/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd
index 34ef37594..b36e35517 100644
--- a/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd
+++ b/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd
@@ -61,6 +61,13 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php
index fc38a7fce..dc1b158e1 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php
@@ -94,6 +94,13 @@ class ActionGroupObject
*/
private $cachedStepKeys = null;
+ /**
+ * Deprecation message.
+ *
+ * @var string|null
+ */
+ private $deprecated;
+
/**
* ActionGroupObject constructor.
*
@@ -103,9 +110,17 @@ class ActionGroupObject
* @param array $actions
* @param string $parentActionGroup
* @param string $filename
+ * @param string|null $deprecated
*/
- public function __construct($name, $annotations, $arguments, $actions, $parentActionGroup, $filename = null)
- {
+ public function __construct(
+ $name,
+ $annotations,
+ $arguments,
+ $actions,
+ $parentActionGroup,
+ $filename = null,
+ $deprecated = null
+ ) {
$this->varAttributes = array_merge(
ActionObject::SELECTOR_ENABLED_ATTRIBUTES,
ActionObject::DATA_ENABLED_ATTRIBUTES
@@ -117,6 +132,17 @@ public function __construct($name, $annotations, $arguments, $actions, $parentAc
$this->parsedActions = $actions;
$this->parentActionGroup = $parentActionGroup;
$this->filename = $filename;
+ $this->deprecated = $deprecated;
+ }
+
+ /**
+ * Returns deprecated messages.
+ *
+ * @return string|null
+ */
+ public function getDeprecated()
+ {
+ return $this->deprecated;
}
/**
@@ -213,7 +239,8 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey)
$action->getLinkedAction() == null ? null : $action->getLinkedAction() . ucfirst($actionReferenceKey),
$orderOffset,
[self::ACTION_GROUP_ORIGIN_NAME => $this->name,
- self::ACTION_GROUP_ORIGIN_TEST_REF => $actionReferenceKey]
+ self::ACTION_GROUP_ORIGIN_TEST_REF => $actionReferenceKey],
+ $action->getDeprecatedUsages()
);
}
@@ -543,10 +570,20 @@ private function addContextCommentsToActionList($actionList, $actionReferenceKey
{
$actionStartComment = self::ACTION_GROUP_CONTEXT_START . "[" . $actionReferenceKey . "] " . $this->name;
$actionEndComment = self::ACTION_GROUP_CONTEXT_END . "[" . $actionReferenceKey . "] " . $this->name;
+
+ $deprecationNotices = [];
+ if ($this->getDeprecated() !== null) {
+ $deprecationNotices[] = "DEPRECATED ACTION GROUP in Test: " . $this->name . ' ' . $this->getDeprecated();
+ }
+
$startAction = new ActionObject(
$actionStartComment,
ActionObject::ACTION_TYPE_COMMENT,
- [ActionObject::ACTION_ATTRIBUTE_USERINPUT => $actionStartComment]
+ [ActionObject::ACTION_ATTRIBUTE_USERINPUT => $actionStartComment],
+ null,
+ ActionObject::MERGE_ACTION_ORDER_BEFORE,
+ null,
+ $deprecationNotices
);
$endAction = new ActionObject(
$actionEndComment,
diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php
index 53743330e..45b5db69d 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php
@@ -84,6 +84,13 @@ class ActionObject
*/
private $stepKey;
+ /**
+ * Array of deprecated entities used in action.
+ *
+ * @var array
+ */
+ private $deprecatedUsage = [];
+
/**
* The type of action (e.g. fillField, createData, etc)
*
@@ -142,6 +149,7 @@ class ActionObject
* @param string|null $linkedAction
* @param string $order
* @param array $actionOrigin
+ * @param array $deprecatedUsage
*/
public function __construct(
$stepKey,
@@ -149,13 +157,15 @@ public function __construct(
$actionAttributes,
$linkedAction = null,
$order = ActionObject::MERGE_ACTION_ORDER_BEFORE,
- $actionOrigin = null
+ $actionOrigin = null,
+ $deprecatedUsage = []
) {
$this->stepKey = $stepKey;
$this->type = $type === self::COMMENT_ACTION ? self::ACTION_TYPE_COMMENT : $type;
$this->actionAttributes = $actionAttributes;
$this->linkedAction = $linkedAction;
$this->actionOrigin = $actionOrigin;
+ $this->deprecatedUsage = $deprecatedUsage;
if ($order === ActionObject::MERGE_ACTION_ORDER_AFTER) {
$this->orderOffset = 1;
@@ -304,7 +314,8 @@ public function trimAssertionAttributes()
if ($appConfig->getPhase() == MftfApplicationConfig::GENERATION_PHASE && $appConfig->verboseEnabled()) {
LoggingUtil::getInstance()->getLogger(ActionObject::class)->deprecation(
"use of one line Assertion actions will be deprecated in MFTF 3.0.0, please use nested syntax",
- ["action" => $this->type, "stepKey" => $this->stepKey]
+ ["action" => $this->type, "stepKey" => $this->stepKey],
+ true
);
}
return;
@@ -534,11 +545,18 @@ private function findAndReplaceReferences($objectHandler, $inputString)
$replacement = null;
$parameterized = false;
} elseif (get_class($obj) == PageObject::class) {
+ if ($obj->getDeprecated() !== null) {
+ $this->deprecatedUsage[] = "DEPRECATED PAGE in Test: " . $match . ' ' . $obj->getDeprecated();
+ }
$this->validateUrlAreaAgainstActionType($obj);
$replacement = $obj->getUrl();
$parameterized = $obj->isParameterized();
} elseif (get_class($obj) == SectionObject::class) {
+ if ($obj->getDeprecated() !== null) {
+ $this->deprecatedUsage[] = "DEPRECATED SECTION in Test: " . $match . ' ' . $obj->getDeprecated();
+ }
list(,$objField) = $this->stripAndSplitReference($match);
+
if ($obj->getElement($objField) == null) {
throw new TestReferenceException(
"Could not resolve entity reference \"{$inputString}\" "
@@ -549,7 +567,15 @@ private function findAndReplaceReferences($objectHandler, $inputString)
$parameterized = $obj->getElement($objField)->isParameterized();
$replacement = $obj->getElement($objField)->getPrioritizedSelector();
$this->setTimeout($obj->getElement($objField)->getTimeout());
+ if ($obj->getElement($objField)->getDeprecated() !== null) {
+ $this->deprecatedUsage[] = "DEPRECATED ELEMENT in Test: " . $match . ' '
+ . $obj->getElement($objField)->getDeprecated();
+ }
} elseif (get_class($obj) == EntityDataObject::class) {
+ if ($obj->getDeprecated() !== null) {
+ $this->deprecatedUsage[] = "DEPRECATED DATA ENTITY in Test: "
+ . $match . ' ' . $obj->getDeprecated();
+ }
$replacement = $this->resolveEntityDataObjectReference($obj, $match);
if (is_array($replacement)) {
@@ -766,4 +792,14 @@ private function checkParameterCount($matches, $parameters, $reference)
);
}
}
+
+ /**
+ * Returns array of deprecated usages in Action.
+ *
+ * @return array
+ */
+ public function getDeprecatedUsages()
+ {
+ return $this->deprecatedUsage;
+ }
}
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php
index 44b004105..9d3a9877e 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php
@@ -64,7 +64,8 @@ private function validateMissingAnnotations($annotationObjects, $filename)
$message = "Action Group File {$filename} is missing required annotations.";
LoggingUtil::getInstance()->getLogger(ActionObject::class)->deprecation(
$message,
- ["actionGroup" => $filename, "missingAnnotations" => implode(", ", $missingAnnotations)]
+ ["actionGroup" => $filename, "missingAnnotations" => implode(", ", $missingAnnotations)],
+ true
);
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php
index e41329cb3..618cddc09 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php
@@ -11,6 +11,8 @@
use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject;
use Magento\FunctionalTestingFramework\Test\Objects\ActionObject;
use Magento\FunctionalTestingFramework\Test\Objects\ArgumentObject;
+use Magento\FunctionalTestingFramework\Test\Objects\TestObject;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
/**
* Class ActionGroupObjectExtractor
@@ -58,7 +60,15 @@ public function __construct()
public function extractActionGroup($actionGroupData)
{
$arguments = [];
+ $deprecated = null;
+ if (array_key_exists(self::OBJ_DEPRECATED, $actionGroupData)) {
+ $deprecated = $actionGroupData[self::OBJ_DEPRECATED];
+ LoggingUtil::getInstance()->getLogger(ActionGroupObject::class)->deprecation(
+ $deprecated,
+ ["actionGroupName" => $actionGroupData[self::FILENAME], "deprecatedActionGroup" => $deprecated]
+ );
+ }
$actionGroupReference = $actionGroupData[self::EXTENDS_ACTION_GROUP] ?? null;
$actionData = $this->stripDescriptorTags(
$actionGroupData,
@@ -69,7 +79,8 @@ public function extractActionGroup($actionGroupData)
self::FILENAME,
self::ACTION_GROUP_INSERT_BEFORE,
self::ACTION_GROUP_INSERT_AFTER,
- self::EXTENDS_ACTION_GROUP
+ self::EXTENDS_ACTION_GROUP,
+ 'deprecated'
);
// TODO filename is now available to the ActionGroupObject, integrate this into debug and error statements
@@ -99,7 +110,8 @@ public function extractActionGroup($actionGroupData)
$arguments,
$actions,
$actionGroupReference,
- $actionGroupData[self::FILENAME]
+ $actionGroupData[self::FILENAME],
+ $deprecated
);
}
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php
index cd81ade24..159822db1 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php
@@ -128,7 +128,8 @@ private function resolveSecretFieldAccess($resolvedActions)
$action->getCustomActionAttributes(),
$action->getLinkedAction(),
$action->getOrderOffset(),
- $action->getActionOrigin()
+ $action->getActionOrigin(),
+ $action->getDeprecatedUsages()
);
$actions[$action->getStepKey()] = $action;
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php
index d30ed351e..1859f3195 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php
@@ -164,7 +164,8 @@ private function validateMissingAnnotations($annotationObjects, $filename)
$message = "Test {$filename} is missing required annotations.";
LoggingUtil::getInstance()->getLogger(ActionObject::class)->deprecation(
$message,
- ["testName" => $filename, "missingAnnotations" => implode(", ", $missingAnnotations)]
+ ["testName" => $filename, "missingAnnotations" => implode(", ", $missingAnnotations)],
+ true
);
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php
index 26644937d..516f79d14 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php
@@ -13,6 +13,7 @@ class BaseObjectExtractor
{
const NODE_NAME = 'nodeName';
const NAME = 'name';
+ const OBJ_DEPRECATED = 'deprecated';
/**
* BaseObjectExtractor constructor.
diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php
index d5420e355..a25db0935 100644
--- a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php
+++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php
@@ -8,13 +8,16 @@
use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
use Magento\FunctionalTestingFramework\Exceptions\XmlException;
+use Magento\FunctionalTestingFramework\Page\Objects\ElementObject;
use Magento\FunctionalTestingFramework\Test\Objects\ActionObject;
use Magento\FunctionalTestingFramework\Test\Objects\TestObject;
+use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
use Magento\FunctionalTestingFramework\Util\ModulePathExtractor;
use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil;
/**
* Class TestObjectExtractor
+ * @SuppressWarnings(PHPMD)
*/
class TestObjectExtractor extends BaseObjectExtractor
{
@@ -95,7 +98,8 @@ public function extractTestData($testData)
$fileNames = explode(",", $filename);
$baseFileName = $fileNames[0];
$module = $this->modulePathExtractor->extractModuleName($baseFileName);
- $testReference = $testData['extends'] ?? null;
+ $testReference = $testData['extends'] ?? null;
+ $deprecated = isset($testData[self::OBJ_DEPRECATED]) ? $testData[self::OBJ_DEPRECATED] : null;
$testActions = $this->stripDescriptorTags(
$testData,
self::NODE_NAME,
@@ -107,6 +111,7 @@ public function extractTestData($testData)
self::TEST_INSERT_BEFORE,
self::TEST_INSERT_AFTER,
self::TEST_FILENAME,
+ self::OBJ_DEPRECATED,
'extends'
);
@@ -122,12 +127,20 @@ public function extractTestData($testData)
// when $fileNames is not available
if (!isset($testAnnotations["description"])) {
$testAnnotations["description"] = [];
+ } else {
+ $testAnnotations["description"]['main'] = $testAnnotations["description"][0];
+ unset($testAnnotations["description"][0]);
+ }
+ $testAnnotations["description"]['test_files'] = $this->appendFileNamesInDescriptionAnnotation($fileNames);
+
+ $testAnnotations["description"][self::OBJ_DEPRECATED] = [];
+ if ($deprecated !== null) {
+ $testAnnotations["description"][self::OBJ_DEPRECATED][] = $deprecated;
+ LoggingUtil::getInstance()->getLogger(TestObject::class)->deprecation(
+ $deprecated,
+ ["testName" => $filename, "deprecatedTest" => $deprecated]
+ );
}
- $description = $testAnnotations["description"][0] ?? '';
- $testAnnotations["description"][0] = $this->appendFileNamesInDescriptionAnnotation(
- $description,
- $fileNames
- );
// extract before
if (array_key_exists(self::TEST_BEFORE_HOOK, $testData) && is_array($testData[self::TEST_BEFORE_HOOK])) {
@@ -152,6 +165,10 @@ public function extractTestData($testData)
);
}
+ if (!empty($testData[self::OBJ_DEPRECATED])) {
+ $testAnnotations[self::OBJ_DEPRECATED] = $testData[self::OBJ_DEPRECATED];
+ }
+
// TODO extract filename info and store
try {
return new TestObject(
@@ -170,14 +187,13 @@ public function extractTestData($testData)
/**
* Append names of test files, including merge files, in description annotation
*
- * @param string $description
- * @param array $fileNames
+ * @param array $fileNames
*
* @return string
*/
- private function appendFileNamesInDescriptionAnnotation($description, $fileNames)
+ private function appendFileNamesInDescriptionAnnotation(array $fileNames)
{
- $description .= '
Test files
';
+ $filePaths = '
Test files
';
foreach ($fileNames as $fileName) {
if (!empty($fileName) && realpath($fileName) !== false) {
@@ -187,11 +203,11 @@ private function appendFileNamesInDescriptionAnnotation($description, $fileNames
DIRECTORY_SEPARATOR
);
if (!empty($relativeFileName)) {
- $description .= $relativeFileName . ' ';
+ $filePaths .= $relativeFileName . ' ';
}
}
}
- return $description;
+ return $filePaths;
}
}
diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd
index 2424d0d31..8670d9885 100644
--- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd
+++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd
@@ -12,6 +12,7 @@
+
@@ -62,6 +63,40 @@
+
+
+
+ Executes Magento Cron Jobs (selected groups)
+
+
+
+
+
+
+
+ Cron groups to be executed (separated by space)
+
+
+
+
+
+
+ Arguments for Magento CLI command, will not be escaped.
+
+
+
+
+
+
+ Idle timeout in seconds, defaulted to 60s when not specified.
+
+
+
+
+
+
+
+
@@ -285,4 +320,4 @@
-
\ No newline at end of file
+
diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd
index 6e9e798f1..bd32ba879 100644
--- a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd
+++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd
@@ -43,6 +43,13 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd
index fe45945ee..6ded366bb 100644
--- a/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd
+++ b/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd
@@ -86,6 +86,13 @@
+
+
+
+ Message and flag which shows that entity is deprecated.
+
+
+
diff --git a/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php b/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php
index 0a52f6b6b..3983a755a 100644
--- a/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php
+++ b/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php
@@ -15,15 +15,17 @@ class MftfLogger extends Logger
/**
* Prints a deprecation warning, as well as adds a log at the WARNING level.
*
- * @param string $message The log message.
- * @param array $context The log context.
+ * @param string $message The log message.
+ * @param array $context The log context.
+ * @param boolean $verbose
* @return void
+ * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException
*/
- public function deprecation($message, array $context = [])
+ public function deprecation($message, array $context = [], $verbose = false)
{
$message = "DEPRECATION: " . $message;
// Suppress print during unit testing
- if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) {
+ if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE && $verbose) {
print ($message . json_encode($context) . "\n");
}
parent::warning($message, $context);
@@ -32,15 +34,17 @@ public function deprecation($message, array $context = [])
/**
* Prints a critical failure, as well as adds a log at the CRITICAL level.
*
- * @param string $message The log message.
- * @param array $context The log context.
+ * @param string $message The log message.
+ * @param array $context The log context.
+ * @param boolean $verbose
* @return void
+ * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException
*/
- public function criticalFailure($message, array $context = [])
+ public function criticalFailure($message, array $context = [], $verbose = false)
{
$message = "FAILURE: " . $message;
// Suppress print during unit testing
- if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) {
+ if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE && $verbose) {
print ($message . implode("\n", $context) . "\n");
}
parent::critical($message, $context);
diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php
index 09c353029..e40f60dcf 100644
--- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php
+++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php
@@ -7,7 +7,6 @@
namespace Magento\FunctionalTestingFramework\Util;
use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig;
-use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler;
use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject;
use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
@@ -17,14 +16,11 @@
use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler;
use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject;
use Magento\FunctionalTestingFramework\Test\Objects\ActionObject;
-use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler;
use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject;
use Magento\FunctionalTestingFramework\Test\Objects\TestObject;
-use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
+use Magento\FunctionalTestingFramework\Test\Util\BaseObjectExtractor;
use Magento\FunctionalTestingFramework\Util\Manifest\BaseTestManifest;
-use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory;
use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor;
-use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor;
use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil;
use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil;
use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter;
@@ -40,9 +36,11 @@ class TestGenerator
const REQUIRED_ENTITY_REFERENCE = 'createDataKey';
const GENERATED_DIR = '_generated';
const DEFAULT_DIR = 'default';
+
const TEST_SCOPE = 'test';
const HOOK_SCOPE = 'hook';
const SUITE_SCOPE = 'suite';
+
const PRESSKEY_ARRAY_ANCHOR_KEY = '987654321098765432109876543210';
const PERSISTED_OBJECT_NOTATION_REGEX = '/\${1,2}[\w.\[\]]+\${1,2}/';
const NO_STEPKEY_ACTIONS = [
@@ -50,10 +48,16 @@ class TestGenerator
'retrieveEntityField',
'getSecret',
'magentoCLI',
+ 'magentoCron',
'generateDate',
'field'
];
+ const RULE_ERROR = 'On step with stepKey "%s", only one of the attributes: "%s" can be use for action "%s"';
+
const STEP_KEY_ANNOTATION = " // stepKey: %s";
+ const CRON_INTERVAL = 60;
+ const ARRAY_WRAP_OPEN = '[';
+ const ARRAY_WRAP_CLOSE = ']';
/**
* Actor name for AcceptanceTest
@@ -105,7 +109,14 @@ class TestGenerator
private $currentGenerationScope = TestGenerator::TEST_SCOPE;
/**
- * TestGenerator constructor.
+ * Test deprecation messages.
+ *
+ * @var array
+ */
+ private $deprecationMessages = [];
+
+ /**
+ * Private constructor for Factory
*
* @param string $exportDir
* @param array $tests
@@ -114,13 +125,11 @@ class TestGenerator
*/
private function __construct($exportDir, $tests, $debug = false)
{
- // private constructor for factory
$this->exportDirName = $exportDir ?? self::DEFAULT_DIR;
- $exportDir = $exportDir ?? self::DEFAULT_DIR;
$this->exportDirectory = FilePathFormatter::format(TESTS_MODULE_PATH)
. self::GENERATED_DIR
. DIRECTORY_SEPARATOR
- . $exportDir;
+ . $this->exportDirName;
$this->tests = $tests;
$this->consoleOutput = new \Symfony\Component\Console\Output\ConsoleOutput();
$this->debug = $debug;
@@ -185,13 +194,13 @@ private function loadAllTestObjects($testsToIgnore)
* @return void
* @throws \Exception
*/
- private function createCestFile($testPhp, $filename)
+ private function createCestFile(string $testPhp, string $filename)
{
$exportFilePath = $this->exportDirectory . DIRECTORY_SEPARATOR . $filename . ".php";
$file = fopen($exportFilePath, 'w');
if (!$file) {
- throw new \Exception("Could not open the file.");
+ throw new \Exception(sprintf('Could not open test file: "%s"', $exportFilePath));
}
fwrite($file, $testPhp);
@@ -238,7 +247,6 @@ public function createAllTestFiles($testManifest = null, $testsToIgnore = null)
public function assembleTestPhp($testObject)
{
$usePhp = $this->generateUseStatementsPhp();
- $classAnnotationsPhp = $this->generateAnnotationsPhp($testObject->getAnnotations());
$className = $testObject->getCodeceptionName();
try {
@@ -251,6 +259,7 @@ public function assembleTestPhp($testObject)
} catch (TestReferenceException $e) {
throw new TestReferenceException($e->getMessage() . "\n" . $testObject->getFilename());
}
+ $classAnnotationsPhp = $this->generateAnnotationsPhp($testObject->getAnnotations());
$cestPhp = "exportDirName . "\Backend;\n\n";
@@ -450,7 +459,7 @@ private function generateMethodAnnotations($annotationType = null, $annotationNa
* Method which return formatted class level annotations based on type and name(s).
*
* @param string $annotationType
- * @param string $annotationName
+ * @param array $annotationName
* @return null|string
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
@@ -464,7 +473,8 @@ private function generateClassAnnotations($annotationType, $annotationName)
break;
case "description":
- $annotationToAppend = sprintf(" * @Description(\"%s\")\n", $annotationName[0]);
+ $template = " * @Description(\"%s\")\n";
+ $annotationToAppend = sprintf($template, $this->generateDescriptionAnnotation($annotationName));
break;
case "testCaseId":
@@ -485,6 +495,36 @@ private function generateClassAnnotations($annotationType, $annotationName)
return $annotationToAppend;
}
+ /**
+ * Generates Description
+ *
+ * @param array $descriptions
+ * @return string
+ */
+ private function generateDescriptionAnnotation(array $descriptions)
+ {
+ $descriptionText = "";
+
+ $descriptionText .= $descriptions["main"] ?? '';
+ if (!empty($descriptions[BaseObjectExtractor::OBJ_DEPRECATED]) || !empty($this->deprecationMessages)) {
+ $deprecatedMessages = array_merge(
+ $descriptions[BaseObjectExtractor::OBJ_DEPRECATED],
+ $this->deprecationMessages
+ );
+
+ $descriptionText .= "