diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index bd50d791a55..6b15d105912 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -21,7 +21,7 @@ jobs: autostart: false # install DDEV configuration - - run: ddev config --project-type=magento --php-version=8.1 --webserver-type=${{ matrix.webserver }} --web-environment="MAGE_IS_DEVELOPER_MODE=1" + - run: ddev config --project-type=magento --php-version=8.1 --webserver-type=${{ matrix.webserver }} --web-environment="MAGE_IS_DEVELOPER_MODE=1,OPENMAGE_CONFIG_OVERRIDE_ALLOWED=1,OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME=ENV name default,OPENMAGE_CONFIG__WEBSITES__BASE__GENERAL__STORE_INFORMATION__PHONE=ENV phone website,OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__ADDRESS=ENV address store" # install composer dependencies - run: ddev composer install diff --git a/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php b/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php index f835ef9f533..b13db6fa277 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php @@ -20,6 +20,8 @@ class Mage_Adminhtml_Block_System_Config_Form extends Mage_Adminhtml_Block_Widge public const SCOPE_STORES = 'stores'; + public const SCOPE_ENV = 'env'; + /** * Config data array * @@ -73,6 +75,7 @@ public function __construct() self::SCOPE_DEFAULT => Mage::helper('adminhtml')->__('[GLOBAL]'), self::SCOPE_WEBSITES => Mage::helper('adminhtml')->__('[WEBSITE]'), self::SCOPE_STORES => Mage::helper('adminhtml')->__('[STORE VIEW]'), + self::SCOPE_ENV => Mage::helper('adminhtml')->__('[ENV]'), ]; } @@ -380,7 +383,7 @@ public function initFields($fieldset, $group, $section, $fieldPrefix = '', $labe } } - $field = $fieldset->addField($id, $fieldType, [ + $elementFieldData = [ 'name' => $name, 'label' => $label, 'comment' => $comment, @@ -396,7 +399,15 @@ public function initFields($fieldset, $group, $section, $fieldPrefix = '', $labe 'scope_label' => $this->getScopeLabel($element), 'can_use_default_value' => $this->canUseDefaultValue((int) $element->show_in_default), 'can_use_website_value' => $this->canUseWebsiteValue((int) $element->show_in_website), - ]); + ]; + if ($this->isOverwrittenByEnvVariable($path)) { + $elementFieldData['scope_label'] = $this->_scopeLabels[self::SCOPE_ENV]; + $elementFieldData['disabled'] = 1; + $elementFieldData['can_use_default_value'] = 0; + $elementFieldData['can_use_website_value'] = 0; + } + + $field = $fieldset->addField($id, $fieldType, $elementFieldData); $this->_prepareFieldOriginalData($field, $element); if (isset($element->validate)) { @@ -645,6 +656,32 @@ public function getScope() return $scope; } + /** + * Returns true if element was overwritten by ENV variable + */ + public function isOverwrittenByEnvVariable(string $path): bool + { + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + + $scope = $this->getScope(); + $store = Mage::app()->getRequest()->getParam('store'); + $website = Mage::app()->getRequest()->getParam('website'); + + if ($store && $website) { + $path = "$scope/$store/$path"; + return $environmentConfigLoaderHelper->hasPath($path); + } + + if ($website) { + $path = "$scope/$website/$path"; + return $environmentConfigLoaderHelper->hasPath($path); + } + + $path = "$scope/$path"; + return $environmentConfigLoaderHelper->hasPath($path); + } + /** * Retrieve label for scope * diff --git a/app/code/core/Mage/Adminhtml/Model/Config/Data.php b/app/code/core/Mage/Adminhtml/Model/Config/Data.php index dd1248c4fb1..a09e91bc686 100644 --- a/app/code/core/Mage/Adminhtml/Model/Config/Data.php +++ b/app/code/core/Mage/Adminhtml/Model/Config/Data.php @@ -355,6 +355,14 @@ protected function _getPathConfig($path, $full = true) } } + if (!$full) { + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + $store = $this->getStore(); + $envConfig = $environmentConfigLoaderHelper->getAsArray($store); + $config = array_merge($config, $envConfig); + } + return $config; } diff --git a/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php index f74676fb5ae..f0c6ac61ad0 100644 --- a/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php +++ b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php @@ -14,15 +14,19 @@ */ class Mage_Core_Helper_EnvironmentConfigLoader extends Mage_Core_Helper_Abstract { - protected const ENV_STARTS_WITH = 'OPENMAGE_CONFIG'; + public const ENV_STARTS_WITH = 'OPENMAGE_CONFIG'; - protected const ENV_KEY_SEPARATOR = '__'; + public const ENV_FEATURE_ENABLED = 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED'; - protected const CONFIG_KEY_DEFAULT = 'DEFAULT'; + public const ENV_KEY_SEPARATOR = '__'; - protected const CONFIG_KEY_WEBSITES = 'WEBSITES'; + public const CONFIG_KEY_DEFAULT = 'DEFAULT'; - protected const CONFIG_KEY_STORES = 'STORES'; + public const CONFIG_KEY_WEBSITES = 'WEBSITES'; + + public const CONFIG_KEY_STORES = 'STORES'; + + public const REGISTRY_KEY = 'current_env_config'; /** * To be used as regex condition @@ -31,6 +35,9 @@ class Mage_Core_Helper_EnvironmentConfigLoader extends Mage_Core_Helper_Abstract protected $_moduleName = 'Mage_Core'; + /** + * @var array + */ protected array $envStore = []; /** @@ -52,11 +59,14 @@ class Mage_Core_Helper_EnvironmentConfigLoader extends Mage_Core_Helper_Abstract * @example OPENMAGE_CONFIG__WEBSITES__BASE__GENERAL__STORE_INFORMATION__NAME=website * Override the store 'german' configuration: * @example OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME=store_german - * - * @return void */ - public function overrideEnvironment(Varien_Simplexml_Config $xmlConfig) + public function overrideEnvironment(Varien_Simplexml_Config $xmlConfig): void { + $data = Mage::registry(self::REGISTRY_KEY); + if ($data) { + return; + } + $env = $this->getEnv(); foreach ($env as $configKey => $value) { @@ -64,27 +74,225 @@ public function overrideEnvironment(Varien_Simplexml_Config $xmlConfig) continue; } - [$configKeyParts, $scope] = $this->getConfigKey($configKey); + $override = $this->getConfigKey($configKey); + $scope = $override->getScope(); + $storeCode = $override->getStoreCode(); + + $path = $this->buildPath($override->getSection(), $override->getGroup(), $override->getField()); switch ($scope) { - case static::CONFIG_KEY_DEFAULT: - [$unused1, $unused2, $section, $group, $field] = $configKeyParts; - $path = $this->buildPath($section, $group, $field); - $xmlConfig->setNode($this->buildNodePath($scope, $path), $value); + case self::CONFIG_KEY_DEFAULT: + try { + $store = Mage::app()->getStore(Mage_Core_Model_Store::ADMIN_CODE); + if ($store instanceof Mage_Core_Model_Store) { + $nodePath = $this->buildNodePath($scope, $path, $storeCode); + $xmlConfig->setNode($nodePath, $value); + $this->setCache($store, $value, $path); + } + + $stores = Mage::app()->getStores(withDefault: true); + foreach ($stores as $store) { + $nodePath = $this->buildNodePath(self::CONFIG_KEY_STORES, $path, $store->getCode()); + $xmlConfig->setNode($nodePath, $value); + $this->setCache($store, $value, $path); + } + } catch (Throwable) { + // invalid store, intentionally empty + } + + break; + + case self::CONFIG_KEY_WEBSITES: + try { + $websites = Mage::app()->getWebsites(); + foreach ($websites as $website) { + if (strtolower($website->getCode()) !== strtolower($storeCode)) { + continue; + } + + $nodePath = $this->buildNodePath($scope, $path, $storeCode); + $xmlConfig->setNode($nodePath, $value); + + $stores = $website->getStores(); + foreach ($stores as $store) { + if ($store instanceof Mage_Core_Model_Store && $store->getId()) { + $nodePath = $this->buildNodePath(self::CONFIG_KEY_STORES, $path, $store->getCode()); + $xmlConfig->setNode($nodePath, $value); + $this->setCache($store, $value, $path); + } + } + } + } catch (Throwable) { + // invalid store, intentionally empty + } + + break; + + case self::CONFIG_KEY_STORES: + try { + $stores = Mage::app()->getStores(); + foreach ($stores as $store) { + if (strtolower($store->getCode()) !== strtolower($storeCode)) { + continue; + } + + $nodePath = $this->buildNodePath($scope, $path, $store->getCode()); + $xmlConfig->setNode($nodePath, $value); + $this->setCache($store, $value, $path); + } + } catch (Throwable) { + // invalid store, intentionally empty + } + + break; + } + } + + try { + Mage::register(self::REGISTRY_KEY, true, true); + } catch (Mage_Core_Exception $mageCoreException) { + Mage::logException($mageCoreException); + } + } + + public function hasPath(string $wantedPath): bool + { + /** @var bool|null $data */ + $data = Mage::registry("config_env_has_path_$wantedPath"); + if ($data !== null) { + return $data; + } + + $env = $this->getEnv(); + $config = []; + + foreach ($env as $configKey => $value) { + if (!$this->isConfigKeyValid($configKey)) { + continue; + } + + $override = $this->getConfigKey($configKey); + $scope = $override->getScope(); + $path = $this->buildPath($override->getSection(), $override->getGroup(), $override->getField()); + + switch ($scope) { + case self::CONFIG_KEY_DEFAULT: + $nodePath = $this->buildNodePath($scope, $path); + $config[$nodePath] = $value; + + try { + $websites = Mage::app()->getWebsites(); + foreach ($websites as $website) { + $nodePath = $this->buildNodePath(self::CONFIG_KEY_WEBSITES, $path, $website->getCode()); + $config[$nodePath] = $value; + } + + $stores = Mage::app()->getStores(withDefault: true); + foreach ($stores as $store) { + $nodePath = $this->buildNodePath(self::CONFIG_KEY_STORES, $path, $store->getCode()); + $config[$nodePath] = $value; + } + } catch (Throwable) { + // invalid store, intentionally empty + } + + break; + + case self::CONFIG_KEY_WEBSITES: + try { + $websites = Mage::app()->getWebsites(); + foreach ($websites as $website) { + if (strtolower($website->getCode()) !== strtolower($override->getStoreCode())) { + continue; + } + + $nodePath = $this->buildNodePath($scope, $path, $website->getCode()); + $config[$nodePath] = $value; + + $stores = $website->getStores(); + foreach ($stores as $store) { + if ($store instanceof Mage_Core_Model_Store && $store->getId()) { + $nodePath = $this->buildNodePath(self::CONFIG_KEY_STORES, $path, $store->getCode()); + $config[$nodePath] = $value; + } + } + } + } catch (Throwable) { + // invalid store, intentionally empty + } + break; + case self::CONFIG_KEY_STORES: + $nodePath = $this->buildNodePath($scope, $path, $override->getStoreCode()); + $config[$nodePath] = $value; - case static::CONFIG_KEY_WEBSITES: - case static::CONFIG_KEY_STORES: - [$unused1, $unused2, $code, $section, $group, $field] = $configKeyParts; - $path = $this->buildPath($section, $group, $field); - $nodePath = sprintf('%s/%s/%s', strtolower($scope), strtolower($code), $path); - $xmlConfig->setNode($nodePath, $value); break; } } + + $hasConfig = array_key_exists($wantedPath, $config); + + try { + Mage::register("config_env_has_path_$wantedPath", $hasConfig); + } catch (Mage_Core_Exception $mageCoreException) { + Mage::logException($mageCoreException); + } + + return $hasConfig; } /** + * @return array + */ + public function getAsArray(string $wantedStore): array + { + if (empty($wantedStore)) { + $wantedStore = 'default'; + } + + /** @var array|null $data */ + $data = Mage::registry("config_env_array_$wantedStore"); + if ($data !== null) { + return $data; + } + + $env = $this->getEnv(); + $config = []; + + foreach ($env as $configKey => $value) { + if (!$this->isConfigKeyValid($configKey)) { + continue; + } + + $override = $this->getConfigKey($configKey); + $path = $this->buildPath($override->getSection(), $override->getGroup(), $override->getField()); + + switch ($override->getScope()) { + case self::CONFIG_KEY_DEFAULT: + $config[$path] = $value; + break; + case self::CONFIG_KEY_WEBSITES: + case self::CONFIG_KEY_STORES: + if (strtolower($override->getStoreCode()) !== strtolower($wantedStore)) { + break; + } + + $config[$path] = $value; + break; + } + } + + try { + Mage::register("config_env_array_$wantedStore", $config); + } catch (Mage_Core_Exception $mageCoreException) { + Mage::logException($mageCoreException); + } + + return $config; + } + + /** + * @param array $envStorage * @internal method mostly for mocking */ public function setEnvStore(array $envStorage): void @@ -92,44 +300,81 @@ public function setEnvStore(array $envStorage): void $this->envStore = $envStorage; } + /** + * @return array + */ public function getEnv(): array { if (empty($this->envStore)) { - $this->envStore = getenv(); + $env = getenv(); + $env = array_filter($env, function ($key) { + return str_starts_with($key, self::ENV_STARTS_WITH); + }, ARRAY_FILTER_USE_KEY); + $this->envStore = $env; + } + + if (!isset($this->envStore[self::ENV_FEATURE_ENABLED]) || + (bool) $this->envStore[self::ENV_FEATURE_ENABLED] === false + ) { + $this->envStore = []; + return $this->envStore; } return $this->envStore; } - protected function getConfigKey(string $configKey): array + protected function setCache(Mage_Core_Model_Store $store, string $value, string $path): void + { + $refObject = new ReflectionObject($store); + $refProperty = $refObject->getProperty('_configCache'); + + $configCache = $refProperty->getValue($store); + if (!is_array($configCache)) { + $configCache = []; + } + + $configCache[$path] = $value; + $store->setConfigCache($configCache); + } + + protected function getConfigKey(string $configKey): Mage_Core_Helper_EnvironmentConfigLoader_Override { $configKeyParts = array_filter( explode( - static::ENV_KEY_SEPARATOR, + self::ENV_KEY_SEPARATOR, $configKey, ), trim(...), ); - [$unused, $scope] = $configKeyParts; - return [$configKeyParts, $scope]; + + $scope = $configKeyParts[1]; + $isDefault = $scope === self::CONFIG_KEY_DEFAULT; + $storeCode = $isDefault ? '' : $configKeyParts[2]; + $section = $isDefault ? $configKeyParts[2] : $configKeyParts[3]; + $group = $isDefault ? $configKeyParts[3] : $configKeyParts[4]; + $field = $isDefault ? $configKeyParts[4] : $configKeyParts[5]; + + return new Mage_Core_Helper_EnvironmentConfigLoader_Override( + scope: $scope, + section: $section, + group: $group, + field: $field, + storeCode: $storeCode, + ); } protected function isConfigKeyValid(string $configKey): bool { - if (!str_starts_with($configKey, static::ENV_STARTS_WITH)) { - return false; - } - - $sectionGroupFieldRegexp = sprintf('([%s]*)', implode('', static::ALLOWED_CHARS)); - $allowedChars = sprintf('[%s]', implode('', static::ALLOWED_CHARS)); - $regexp = '/' . static::ENV_STARTS_WITH . static::ENV_KEY_SEPARATOR . '(WEBSITES' . static::ENV_KEY_SEPARATOR - . $allowedChars . '+|DEFAULT|STORES' . static::ENV_KEY_SEPARATOR . $allowedChars . '+)' - . static::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp - . static::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp - . static::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp . '/'; - // /OPENMAGE_CONFIG__(WEBSITES__[A-Z-_]+|DEFAULT|STORES__[A-Z-_]+)__([A-Z-_]*)__([A-Z-_]*)__([A-Z-_]*)/ + $sectionGroupFieldRegexp = sprintf('([%s]*)', implode('', self::ALLOWED_CHARS)); + $allowedChars = sprintf('[%s]', implode('', self::ALLOWED_CHARS)); + $regexp = '/' . self::ENV_STARTS_WITH . self::ENV_KEY_SEPARATOR . '(WEBSITES' . self::ENV_KEY_SEPARATOR + . '[A-Z][A-Z0-9]' . '+|DEFAULT|STORES' . self::ENV_KEY_SEPARATOR . $allowedChars . '+)' + . self::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp + . self::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp + . self::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp . '/'; + // /OPENMAGE_CONFIG__(WEBSITES__[A-Z][A-Z0-9]+|DEFAULT|STORES__[A-Z-_]+)__([A-Z-_]*)__([A-Z-_]*)__([A-Z-_]*)/ - return preg_match($regexp, $configKey); + return (bool) preg_match($regexp, $configKey); } /** @@ -143,8 +388,8 @@ protected function buildPath(string $section, string $group, string $field): str /** * Build configuration node path. */ - protected function buildNodePath(string $scope, string $path): string + protected function buildNodePath(string $scope, string $path, string $storeCode = ''): string { - return strtolower($scope) . '/' . $path; + return strtolower($scope) . ($storeCode ? '/' . strtolower($storeCode) : '') . '/' . $path; } } diff --git a/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader/Override.php b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader/Override.php new file mode 100644 index 00000000000..653669e1eae --- /dev/null +++ b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader/Override.php @@ -0,0 +1,97 @@ +scope = $scope; + $this->section = $section; + $this->group = $group; + $this->field = $field; + $this->storeCode = $storeCode; + } + + public function setScope(string $scope): self + { + $this->scope = $scope; + return $this; + } + + public function getScope(): string + { + return $this->scope; + } + + public function setStoreCode(string $storeCode): self + { + $this->storeCode = $storeCode; + return $this; + } + + public function getStoreCode(): string + { + return $this->storeCode; + } + + public function setSection(string $section): self + { + $this->section = $section; + return $this; + } + + public function getSection(): string + { + return $this->section; + } + + public function setGroup(string $group): self + { + $this->group = $group; + return $this; + } + + public function getGroup(): string + { + return $this->group; + } + + public function setField(string $field): self + { + $this->field = $field; + return $this; + } + + public function getField(): string + { + return $this->field; + } +} diff --git a/app/code/core/Mage/Core/Model/App.php b/app/code/core/Mage/Core/Model/App.php index 4c5091607bb..d830cdb252a 100644 --- a/app/code/core/Mage/Core/Model/App.php +++ b/app/code/core/Mage/Core/Model/App.php @@ -634,6 +634,7 @@ public function reinitStores() */ protected function _initStores() { + Mage::unregister('current_env_config'); $this->_stores = []; $this->_groups = []; $this->_website = null; diff --git a/app/code/core/Mage/Core/Model/Store.php b/app/code/core/Mage/Core/Model/Store.php index cef474600e5..346203bddbd 100644 --- a/app/code/core/Mage/Core/Model/Store.php +++ b/app/code/core/Mage/Core/Model/Store.php @@ -365,6 +365,9 @@ public function getConfig($path) } $config = Mage::getConfig(); + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + $environmentConfigLoaderHelper->overrideEnvironment($config); $fullPath = 'stores/' . $this->getCode() . '/' . $path; $data = $config->getNode($fullPath); diff --git a/cypress/e2e/openmage/backend/system/config/general/general.cy.js b/cypress/e2e/openmage/backend/system/config/general/general.cy.js new file mode 100644 index 00000000000..b04a627cb8b --- /dev/null +++ b/cypress/e2e/openmage/backend/system/config/general/general.cy.js @@ -0,0 +1,70 @@ +const test = cy.openmage.test.backend.system.config.general.general.config; +const validation = cy.openmage.validation; + +describe(`Checks admin system "${test.section.title}" settings`, () => { + beforeEach('Log in the user', () => { + cy.openmage.admin.login(); + cy.openmage.admin.goToPage(test, test.section); + cy.openmage.admin.goToSection(test.section); + }); + + const fields = test.section.storeInformation.__fields; + const labelEnv = '[ENV]'; + const labelStoreView = '[STORE VIEW]'; + + it(`tests ENV override`, () => { + // test overrides in default config scope + cy.get(fields.name._).should('have.value', 'ENV name default'); + cy.get(fields.name.label).should('have.text', labelEnv); + + cy.get(fields.phone._).should('have.value', ''); + cy.get(fields.phone.label).should('have.text', labelStoreView); + + cy.get(fields.address._).should('have.value', ''); + cy.get(fields.address.label).should('have.text', labelStoreView); + + // test overrides in website scope + cy.openmage.admin.goToConfigScope(test.section, 'website_base'); + cy.get(fields.name._).should('have.value', 'ENV name default'); + cy.get(fields.name.label).should('have.text', labelEnv); + + cy.get(fields.phone._).should('have.value', 'ENV phone website'); + cy.get(fields.phone.label).should('have.text', labelEnv); + + cy.get(fields.address._).should('have.value', ''); + cy.get(fields.address.label).should('have.text', labelStoreView); + + // test overrides in English/default store view + cy.openmage.admin.goToConfigScope(test.section, 'store_default'); + cy.get(fields.name._).should('have.value', 'ENV name default'); + cy.get(fields.name.label).should('have.text', labelEnv); + + cy.get(fields.phone._).should('have.value', 'ENV phone website'); + cy.get(fields.phone.label).should('have.text', labelEnv); + + cy.get(fields.address._).should('have.value', ''); + cy.get(fields.address.label).should('have.text', labelStoreView); + + // test overrides in French store view + cy.openmage.admin.goToConfigScope(test.section, 'store_french'); + cy.get(fields.name._).should('have.value', 'ENV name default'); + cy.get(fields.name.label).should('have.text', labelEnv); + + cy.get(fields.phone._).should('have.value', 'ENV phone website'); + cy.get(fields.phone.label).should('have.text', labelEnv); + + cy.get(fields.address._).should('have.value', ''); + cy.get(fields.address.label).should('have.text', labelStoreView); + + // test overrides in German store view + cy.openmage.admin.goToConfigScope(test.section, 'store_german'); + cy.get(fields.name._).should('have.value', 'ENV name default'); + cy.get(fields.name.label).should('have.text', labelEnv); + + cy.get(fields.phone._).should('have.value', 'ENV phone website'); + cy.get(fields.phone.label).should('have.text', labelEnv); + + cy.get(fields.address._).should('have.value', 'ENV address store'); + cy.get(fields.address.label).should('have.text', labelEnv); + }); +}); \ No newline at end of file diff --git a/cypress/e2e/openmage/frontend/customer/account.cy.js b/cypress/e2e/openmage/frontend/customer/account.cy.js index 5f3de042e87..2fbf105621a 100644 --- a/cypress/e2e/openmage/frontend/customer/account.cy.js +++ b/cypress/e2e/openmage/frontend/customer/account.cy.js @@ -48,7 +48,9 @@ describe('Checks customer account create', () => { const lastname = 'Doe'; const password = '12345678'; - const message = 'Thank you for registering with Madison Island.'; + // see PR: https://github.com/OpenMage/magento-lts/pull/4617 + // const message = 'Thank you for registering with Madison Island.'; + const message = 'Thank you for registering with ENV name default.'; const filename = 'message.customer.account.create.success'; cy.get(test.create.__fields.firstname._).type(firstname).should('have.value', firstname); cy.get(test.create.__fields.lastname._).type(lastname).should('have.value', lastname); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index f5ac2ca454f..aabf07d5eaa 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -52,6 +52,7 @@ import './openmage/backend/system/cache' import './openmage/backend/system/config/catalog/configswatches' import './openmage/backend/system/config/catalog/sitemap' import './openmage/backend/system/config/customer/promo' +import './openmage/backend/system/config/general/general' import './openmage/backend/system/currency' import './openmage/backend/system/design' import './openmage/backend/system/email' diff --git a/cypress/support/openmage/_utils/admin.js b/cypress/support/openmage/_utils/admin.js index 32c1d4cae78..d3f31832fe2 100644 --- a/cypress/support/openmage/_utils/admin.js +++ b/cypress/support/openmage/_utils/admin.js @@ -41,6 +41,13 @@ cy.openmage.admin = { cy.get(section._).click({force: true}); cy.url().should('include', section.url); }, + goToConfigScope: (section, value) =>{ + cy.log('Go to store switcher config scope'); + cy.log(`Clicking on "${value}" menu`); + const selector = 'select#store_switcher'; + cy.get(selector).select(value); // acts like clicking + cy.url().should('include', section.url); + }, } cy.openmage.admin.username = { diff --git a/cypress/support/openmage/_utils/test.js b/cypress/support/openmage/_utils/test.js index 1f1d48ce150..597889f4d18 100644 --- a/cypress/support/openmage/_utils/test.js +++ b/cypress/support/openmage/_utils/test.js @@ -287,6 +287,8 @@ cy.openmage.test.backend.system.config.catalog.configswatches = {}; cy.openmage.test.backend.system.config.catalog.sitemap = {}; cy.openmage.test.backend.system.config.customer = {}; cy.openmage.test.backend.system.config.customer.promo = {}; +cy.openmage.test.backend.system.config.general = {}; +cy.openmage.test.backend.system.config.general.general = {}; cy.openmage.test.backend.system.currency = {}; cy.openmage.test.backend.system.design = {}; cy.openmage.test.backend.system.email = {}; diff --git a/cypress/support/openmage/_utils/validation.js b/cypress/support/openmage/_utils/validation.js index d542153be5d..1dd361a0f7c 100644 --- a/cypress/support/openmage/_utils/validation.js +++ b/cypress/support/openmage/_utils/validation.js @@ -20,10 +20,13 @@ cy.openmage.validation = { cy.log('Filling fields with invalid values'); Object.keys(path.__fields).forEach(field => { const selector = path.__fields[field]._; - cy - .get(selector) - .clear({ force: true }) - .should('have.class', validation.css); + + if (validation.css !== undefined) { + cy + .get(selector) + .clear({ force: true }) + .should('have.class', validation.css); + } if (value !== '') { cy diff --git a/cypress/support/openmage/backend/system/config/general/general.js b/cypress/support/openmage/backend/system/config/general/general.js new file mode 100644 index 00000000000..68e2936a3f3 --- /dev/null +++ b/cypress/support/openmage/backend/system/config/general/general.js @@ -0,0 +1,50 @@ +const base = cy.openmage.test.backend.__base; +const test = cy.openmage.test.backend.system.config.general.general; + +/** + * Configuration for admin system "General" settings + * @type {{_: string, _nav: string, _title: string, url: string, section: {}}} + */ +test.config = { + _: '#nav-admin-system-config', + _nav: '#nav-admin-system', + _title: base._title, + url: 'system_config/index', + section: {}, +} + +/** + * Section "General" + * @type {{_: string, title: string, url: string}} + */ +test.config.section = { + _: '#section-general', + title: 'General', + url: 'system_config/edit/section/general', +} + +/** + * Fields for "Store information" group + * @type {{__fields: {name: {_: string}, phone: {_: string}, hours: {_: string}, address: {_: string}}}} + */ +test.config.section.storeInformation = { + _: '#general_store_information-head', + __fields: { + name: { + _: '#general_store_information_name', + label: '#row_general_store_information_name .scope-label', + }, + phone: { + _: '#general_store_information_phone', + label: '#row_general_store_information_phone .scope-label', + }, + hours: { + _: '#general_store_information_hours', + label: '#row_general_store_information_hours .scope-label', + }, + address: { + _: '#general_store_information_address', + label: '#row_general_store_information_address .scope-label', + }, + } +}; diff --git a/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php b/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php index 2981c4cb182..4f74a820788 100644 --- a/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php +++ b/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php @@ -18,6 +18,9 @@ use OpenMage\Tests\Unit\OpenMageTest; use Varien_Simplexml_Config; +/** + * @group Mage_Core_EnvLoader + */ final class EnvironmentConfigLoaderTest extends OpenMageTest { public const XML_PATH_GENERAL = 'general/store_information/name'; @@ -28,12 +31,55 @@ final class EnvironmentConfigLoaderTest extends OpenMageTest public const XML_PATH_STORE = 'stores/german/general/store_information/name'; + private const ENV_TEST_STORES = ['german_ch', 'german', 'german-at']; + + private string $testXml; + + private const WEBSITES = [ + 'base' => self::ENV_TEST_STORES, + ]; + + private static array $storeData = []; + /** * @throws Mage_Core_Exception */ public function setup(): void { Mage::setRoot(); + $this->testXml = $this->getTestXml(); + Mage::unregister(Mage_Core_Helper_EnvironmentConfigLoader::REGISTRY_KEY); + } + + public static function setUpBeforeClass(): void + { + Mage::app('admin'); + foreach (self::WEBSITES as $websiteCode => $stores) { + foreach ($stores as $storeCode) { + self::$storeData[$websiteCode][$storeCode] = + self::bootstrapTestStore($websiteCode, $storeCode); + } + } + } + + public static function tearDownAfterClass(): void + { + foreach (array_keys(self::WEBSITES) as $websiteCode) { + self::cleanupTestWebsite($websiteCode); + } + } + + public function testStoresAreCreated(): void + { + foreach (self::$storeData as $stores) { + foreach ($stores as $storeCode => $data) { + $store = Mage::app()->getStore($data['store_id']); + self::assertInstanceOf(\Mage_Core_Model_Store::class, $store); + self::assertTrue((bool) $store->getIsActive(), "$storeCode is not active"); + self::assertEquals($data['store_id'], (int) $store->getId()); + self::assertEquals($data['website_id'], (int) $store->getWebsiteId()); + } + } } /** @@ -49,6 +95,32 @@ public function testBuildPath(): void /** * @group Helper */ + public function testEnvFilter(): void + { + $environmentConfigLoaderHelper = new EnvironmentConfigLoaderTestHelper(); + /** @phpstan-ignore method.internal */ + $environmentConfigLoaderHelper->setEnvStore([ + 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME' => 'some_value', + ]); + // empty because env flag is not set + $env = $environmentConfigLoaderHelper->getEnv(); + self::assertIsArray($env); + self::assertEmpty($env); + /** @phpstan-ignore method.internal */ + $environmentConfigLoaderHelper->setEnvStore([ + 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME' => 'some_value', + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, // enable feature + ]); + // flag is set => feature is enabled + $env = $environmentConfigLoaderHelper->getEnv(); + self::assertIsArray($env); + self::assertNotEmpty($env); + } + + /** + * @group Mage_Core + * @group Mage_Core_Helper + */ public function testBuildNodePath(): void { $environmentConfigLoaderHelper = new EnvironmentConfigLoaderTestHelper(); @@ -61,15 +133,15 @@ public function testBuildNodePath(): void */ public function testXmlHasTestStrings(): void { - $xmlStruct = $this->getTestXml(); $xml = new Varien_Simplexml_Config(); - $xml->loadString($xmlStruct); + $xml->loadString($this->testXml); self::assertSame('test_default', (string) $xml->getNode(self::XML_PATH_DEFAULT)); self::assertSame('test_website', (string) $xml->getNode(self::XML_PATH_WEBSITE)); self::assertSame('test_store', (string) $xml->getNode(self::XML_PATH_STORE)); } /** + * @runInSeparateProcess * @dataProvider envOverridesCorrectConfigKeysDataProvider * @group Helper * @@ -77,17 +149,17 @@ public function testXmlHasTestStrings(): void */ public function testEnvOverridesForValidConfigKeys(array $config): void { - $xmlStruct = $this->getTestXml(); - $xmlDefault = new Varien_Simplexml_Config(); - $xmlDefault->loadString($xmlStruct); + $xmlDefault->loadString($this->testXml); $xml = new Varien_Simplexml_Config(); - $xml->loadString($xmlStruct); + $xml->loadString($this->testXml); + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); /** @phpstan-ignore method.internal */ $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, $config['env_path'] => $config['value'], ]); $loader->overrideEnvironment($xml); @@ -97,7 +169,9 @@ public function testEnvOverridesForValidConfigKeys(array $config): void $valueAfterOverride = $xml->getNode($configPath); // assert - self::assertNotSame((string) $defaultValue, (string) $valueAfterOverride, 'Default value was not overridden.'); + $expected = (string) $defaultValue; + $actual = (string) $valueAfterOverride; + self::assertNotSame($expected, $actual, 'Default value was not overridden.'); } public function envOverridesCorrectConfigKeysDataProvider(): Generator @@ -107,7 +181,6 @@ public function envOverridesCorrectConfigKeysDataProvider(): Generator $defaultPathWithUnderscore = 'OPENMAGE_CONFIG__DEFAULT__GENERAL__FOO_BAR__NAME'; $websitePath = 'OPENMAGE_CONFIG__WEBSITES__BASE__GENERAL__STORE_INFORMATION__NAME'; - $websiteWithDashPath = 'OPENMAGE_CONFIG__WEBSITES__BASE-AT__GENERAL__STORE_INFORMATION__NAME'; $websiteWithUnderscorePath = 'OPENMAGE_CONFIG__WEBSITES__BASE_CH__GENERAL__STORE_INFORMATION__NAME'; $storeWithDashPath = 'OPENMAGE_CONFIG__STORES__GERMAN-AT__GENERAL__STORE_INFORMATION__NAME'; @@ -156,21 +229,107 @@ public function envOverridesCorrectConfigKeysDataProvider(): Generator 'env_path' => $websitePath, 'value' => 'website_new_value', ]]; - yield 'Case WEBSITE overrides #2.' => [[ - 'case' => 'WEBSITE', - 'xml_path' => 'websites/base_ch/general/store_information/name', - 'env_path' => $websiteWithUnderscorePath, - 'value' => 'website_new_value', - ]]; - yield 'Case WEBSITE overrides #3.' => [[ - 'case' => 'WEBSITE', - 'xml_path' => 'websites/base-at/general/store_information/name', - 'env_path' => $websiteWithDashPath, - 'value' => 'website_new_value', - ]]; } /** + * @runInSeparateProcess + * @dataProvider envAsArrayDataProvider + * @group Mage_Core + * + * @param array $config + */ + public function testAsArray(array $config): void + { + // phpcs:ignore Ecg.Classes.ObjectInstantiation.DirectInstantiation + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); + /** @phpstan-ignore method.internal */ + $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, + $config['env_path'] => 1, + ]); + $store = $config['store']; + $actual = $loader->getAsArray($store); + $expected = $config['expected']; + self::assertSame($expected, $actual); + } + + public function envAsArrayDataProvider(): Generator + { + yield 'default' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME', + 'store' => '', // or 'default', which will be used internally, but this is how \Mage_Adminhtml_Model_Config_Data::_validate defines it + 'expected' => [ + self::XML_PATH_GENERAL => 1, + ], + ], + ]; + yield 'store' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME', + 'store' => 'german', + 'expected' => [ + self::XML_PATH_GENERAL => 1, + ], + ], + ]; + yield 'invalidStore' => [ + [ + 'env_path' => '', + 'store' => 'foo', + 'expected' => [], + ], + ]; + } + + /** + * @runInSeparateProcess + * @dataProvider envHasPathDataProvider + * @group Mage_Core + * + * @param array $config + */ + public function testHasPath(array $config): void + { + // phpcs:ignore Ecg.Classes.ObjectInstantiation.DirectInstantiation + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); + /** @phpstan-ignore method.internal */ + $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, + $config['env_path'] => 1, + ]); + $actual = $loader->hasPath($config['xml_path']); + $expected = $config['expected']; + self::assertSame($expected, $actual); + } + + public function envHasPathDataProvider(): Generator + { + yield 'hasPath default' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME', + 'xml_path' => 'default/general/store_information/name', + 'expected' => true, + ], + ]; + yield 'hasPath store' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME', + 'xml_path' => 'stores/german/general/store_information/name', + 'expected' => true, + ], + ]; + yield 'hasNotPath' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME', + 'xml_path' => 'foo/foo/foo', + 'expected' => false, + ], + ]; + } + + /** + * @runInSeparateProcess * @dataProvider envDoesNotOverrideOnWrongConfigKeysDataProvider * @group Helper * @@ -187,15 +346,19 @@ public function testEnvDoesNotOverrideForInvalidConfigKeys(array $config): void $xml->loadString($xmlStruct); $defaultValue = 'test_default'; - self::assertSame($defaultValue, (string) $xml->getNode(self::XML_PATH_DEFAULT)); + $actual = (string) $xml->getNode(self::XML_PATH_DEFAULT); + self::assertSame($defaultValue, $actual); $defaultWebsiteValue = 'test_website'; - self::assertSame($defaultWebsiteValue, (string) $xml->getNode(self::XML_PATH_WEBSITE)); + $actual = (string) $xml->getNode(self::XML_PATH_WEBSITE); + self::assertSame($defaultWebsiteValue, $actual); $defaultStoreValue = 'test_store'; - self::assertSame($defaultStoreValue, (string) $xml->getNode(self::XML_PATH_STORE)); + $actual = (string) $xml->getNode(self::XML_PATH_STORE); + self::assertSame($defaultStoreValue, $actual); $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); /** @phpstan-ignore method.internal */ $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, $config['path'] => $config['value'], ]); $loader->overrideEnvironment($xml); @@ -258,7 +421,7 @@ public function getTestXml(): string - + @@ -267,13 +430,6 @@ public function getTestXml(): string - - - - test_website - - - @@ -308,4 +464,54 @@ public function getTestXml(): string XML; } + + private static function bootstrapTestStore(string $websiteCode, string $storeCode): array + { + $website = Mage::getModel('core/website')->load($websiteCode, 'code'); + if (!$website->getId()) { + $website->setCode($websiteCode) + ->setName(ucfirst($websiteCode) . ' Website') + ->save(); + } + + $storeGroup = Mage::getModel('core/store_group') + ->getCollection() + ->addFieldToFilter('website_id', $website->getId()) + ->getFirstItem(); + + $store = Mage::getModel('core/store')->load($storeCode, 'code'); + if (!$store->getId()) { + $store->setCode($storeCode) + ->setWebsiteId((int) $website->getId()) + ->setGroupId((int) $storeGroup->getId()) + ->setName(ucfirst($storeCode) . ' Store -- ENVTEST') + ->setIsActive(1) + ->save(); + } + + Mage::app()->cleanCache(); + Mage::app()->reinitStores(); + return [ + 'website_id' => (int) $website->getId(), + 'store_group_id' => (int) $storeGroup->getId(), + 'store_id' => (int) $store->getId(), + ]; + } + + private static function cleanupTestWebsite(string $websiteCode): void + { + $website = Mage::getModel('core/website')->load($websiteCode, 'code'); + $stores = Mage::getModel('core/store') + ->getCollection() + ->addFieldToFilter('website_id', $website->getId()) + ->addFieldToFilter('code', [ + 'in' => self::ENV_TEST_STORES, + ]); + foreach ($stores as $store) { + $store->delete(); + } + + Mage::app()->cleanCache(); + Mage::app()->reinitStores(); + } }