Skip to content

Commit 94b9fde

Browse files
committed
feat: add ScopeConfigurationInterface fake
fix: default `Store/StoreManager` now maps 1-to-1 to default store view in magento fix: README.md contains now new expected features
1 parent 26a5cca commit 94b9fde

File tree

6 files changed

+328
-8
lines changed

6 files changed

+328
-8
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ class YourTestedClassTest extends \PHPUnit\Framework\TestCase
7272
- [x] `StoreInterface` implementation as a simple data object for testing store related behaviour
7373
- [x] `GroupInterface` implementation as a simple data object for testing store group related behaviour
7474
- [x] `WebsiteInterface` implementation as a simple data object for testing website related behaviour
75-
- [ ] `ScopeConfigurationInterface` implementation for testing configuration dependent functionality
76-
- [ ] `ShoppingCartInterface` implementation for testing shopping cart related functionalities
77-
- [ ] `CustomerInterface` implementation for testing customer related functionalities
78-
- [ ] `Customer/Session` implementation for testing functionalities that depend on current customer data
79-
- [ ] `Checkout/Session` implementation for testing functionalities that depend on current checkout data
75+
- [x] `ScopeConfigurationInterface` implementation for testing configuration dependent functionality
76+
- [ ] `DeploymentConfig` implementation for using in configuration caches, db connections, http cache, etc
77+
- [ ] `ResourceConnection` implementation for quick testing of database components

src/ArrayConfig.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace EcomDev\Magento2TestEssentials;
4+
5+
/**
6+
* Internal class used as storage for configuration
7+
*
8+
* @internal
9+
*/
10+
final class ArrayConfig
11+
{
12+
private function __construct(private readonly array $data = [])
13+
{
14+
}
15+
16+
public static function new(): self
17+
{
18+
return new self();
19+
}
20+
21+
public function withValue(string $scope, string $path, mixed $value): self
22+
{
23+
$parts = explode('/', $path);
24+
$data = $this->data;
25+
$current = &$data[$scope];
26+
foreach ($parts as $part) {
27+
$current = &$current[$part];
28+
}
29+
$current = $value;
30+
return new self($data);
31+
}
32+
33+
public function getValueByPath(string $scope, string $path): mixed
34+
{
35+
$parts = explode('/', $path);
36+
$current = $this->data[$scope] ?? [];
37+
foreach ($parts as $part) {
38+
if (!isset($current[$part])) {
39+
return null;
40+
}
41+
$current = $current[$part];
42+
}
43+
return $current;
44+
}
45+
}

src/ScopeConfig.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
namespace EcomDev\Magento2TestEssentials;
4+
5+
use EcomDev\Magento2TestEssentials\Store\StoreManager;
6+
use Magento\Framework\App\Config\ScopeConfigInterface;
7+
use Magento\Store\Model\ScopeInterface;
8+
9+
/**
10+
* Fake implementation of scoped config
11+
*
12+
* Allows testing functionalities
13+
* that rely on custom configuration value to change its behaviour
14+
*/
15+
final class ScopeConfig implements ScopeConfigInterface
16+
{
17+
private function __construct(
18+
private readonly StoreManager $storeManager,
19+
private readonly ArrayConfig $data,
20+
) {
21+
}
22+
23+
public static function new(): self
24+
{
25+
return new self(StoreManager::new(), ArrayConfig::new());
26+
}
27+
28+
/**
29+
* Configures custom storage manager for testing
30+
*/
31+
public function withStoreManager(StoreManager $storeManager): self
32+
{
33+
return new self($storeManager, $this->data);
34+
}
35+
36+
/**
37+
* Adds configuration value in the specific store
38+
*/
39+
public function withStoreValue(string $code, string $path, mixed $value): self
40+
{
41+
return new self(
42+
$this->storeManager,
43+
$this->data->withValue(
44+
sprintf('%s/%s', ScopeInterface::SCOPE_STORES, $code),
45+
$path,
46+
$value
47+
)
48+
);
49+
}
50+
51+
/**
52+
* Adds configuration value in the specific website
53+
*/
54+
public function withWebsiteValue(string $code, string $path, mixed $value): self
55+
{
56+
return new self(
57+
$this->storeManager,
58+
$this->data->withValue(
59+
sprintf('%s/%s', ScopeInterface::SCOPE_WEBSITES, $code),
60+
$path,
61+
$value
62+
)
63+
);
64+
}
65+
66+
/**
67+
* Adds configuration value for default scope
68+
*/
69+
public function withDefaultValue(string $path, mixed $value): self
70+
{
71+
return new self(
72+
$this->storeManager,
73+
$this->data->withValue(
74+
ScopeConfigInterface::SCOPE_TYPE_DEFAULT,
75+
$path,
76+
$value
77+
)
78+
);
79+
}
80+
81+
/**
82+
* Returns merged configuration value for requested scope and path
83+
*/
84+
public function getValue($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null): mixed
85+
{
86+
$scope = match ($scopeType) {
87+
ScopeInterface::SCOPE_WEBSITE, ScopeInterface::SCOPE_WEBSITES => sprintf(
88+
'%s/%s',
89+
ScopeInterface::SCOPE_WEBSITES, $scopeCode
90+
),
91+
ScopeInterface::SCOPE_STORE, ScopeInterface::SCOPE_STORES => sprintf(
92+
'%s/%s',
93+
ScopeInterface::SCOPE_STORES, $scopeCode
94+
),
95+
default => ScopeConfigInterface::SCOPE_TYPE_DEFAULT,
96+
};
97+
98+
$fallbackValue = match ($scopeType) {
99+
ScopeInterface::SCOPE_WEBSITES, ScopeInterface::SCOPE_WEBSITE => $this->getValue($path, ScopeConfigInterface::SCOPE_TYPE_DEFAULT),
100+
ScopeInterface::SCOPE_STORES, ScopeInterface::SCOPE_STORE => $this->getValue(
101+
$path,
102+
ScopeInterface::SCOPE_WEBSITE,
103+
$this->storeManager->getWebsite(
104+
$this->storeManager->getStore($scopeCode)->getWebsiteId()
105+
)->getCode()
106+
),
107+
default => null,
108+
};
109+
110+
$value = $this->data->getValueByPath($scope, $path);
111+
112+
if (is_array($value) && is_array($fallbackValue)) {
113+
return array_replace_recursive($fallbackValue, $value);
114+
}
115+
116+
return $value ?? $fallbackValue;
117+
}
118+
119+
/**
120+
* Checks if truthy value is set in the configuration
121+
*/
122+
public function isSetFlag($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null): bool
123+
{
124+
return !!$this->getValue($path, $scopeType, $scopeCode);
125+
}
126+
}

src/Store/StoreManager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class StoreManager implements StoreManagerInterface
5454
public static function new(): self
5555
{
5656
return (new self())
57-
->withWebsite(Website::new(0, 'base'))
57+
->withWebsite(Website::new(0, 'admin'))
5858
->withStore(Store::new(0, 'admin'))
5959
->withGroup(StoreGroup::new(0, 'admin'));
6060
}

tests/ScopeConfigTest.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
/**
4+
* Copyright © EcomDev B.V. All rights reserved.
5+
* See LICENSE for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace EcomDev\Magento2TestEssentials;
11+
12+
use EcomDev\Magento2TestEssentials\Store\Store;
13+
use EcomDev\Magento2TestEssentials\Store\StoreManager;
14+
use EcomDev\Magento2TestEssentials\Store\Website;
15+
use Magento\Store\Model\ScopeInterface;
16+
use PHPUnit\Framework\TestCase;
17+
use PHPUnit\Framework\Attributes\Test;
18+
19+
class ScopeConfigTest extends TestCase
20+
{
21+
#[Test]
22+
public function simpleDefaultValueRetrieval()
23+
{
24+
$this->assertEquals(
25+
'test',
26+
ScopeConfig::new()
27+
->withDefaultValue('some/thing/enabled', 'test')
28+
->getValue('some/thing/enabled')
29+
);
30+
}
31+
32+
#[Test]
33+
public function defaultValueOverridenByWebsiteScope()
34+
{
35+
$this->assertEquals(
36+
'base_website_value',
37+
ScopeConfig::new()
38+
->withStoreManager(
39+
StoreManager::new()
40+
->withWebsite(
41+
Website::new(1, 'base')
42+
)
43+
)
44+
->withDefaultValue('some/other/value', 'default_value')
45+
->withWebsiteValue('base', 'some/other/value', 'base_website_value')
46+
->withWebsiteValue('default', 'some/other/value', 'default_website_value')
47+
->getValue('some/other/value', ScopeInterface::SCOPE_WEBSITE, 'base')
48+
);
49+
}
50+
51+
#[Test]
52+
public function configurationValuesMergedFromRightStoreChain()
53+
{
54+
$this->assertEquals(
55+
[
56+
'path_one' => 'default_one_value',
57+
'path_two' => 'base_website_value',
58+
'path_three' => 'default_store_value',
59+
],
60+
ScopeConfig::new()
61+
->withStoreManager(
62+
StoreManager::new()
63+
->withWebsite(
64+
Website::new(1, 'base')
65+
)
66+
->withStore(
67+
Store::new(1, 'default')
68+
->withWebsite(1, 1)
69+
)
70+
->withStore(
71+
Store::new(2, 'english')
72+
->withWebsite(1, 1)
73+
)
74+
)
75+
->withDefaultValue('some/other/path_one', 'default_one_value')
76+
->withDefaultValue('some/other/path_two', 'default_one_value')
77+
->withDefaultValue('some/other/path_three', 'default_one_value')
78+
->withWebsiteValue('base', 'some/other/path_two', 'base_website_value')
79+
->withStoreValue('default', 'some/other/path_three', 'default_store_value')
80+
->withStoreValue('english', 'some/other/path_three', 'eng_store_value')
81+
->getValue('some/other', ScopeInterface::SCOPE_STORE, 'default')
82+
);
83+
}
84+
85+
#[Test]
86+
public function mergesValuesOnlyIfBothPathsAreArraysOrOneIsNull()
87+
{
88+
$config = ScopeConfig::new()
89+
->withStoreManager(
90+
StoreManager::new()
91+
->withWebsite(
92+
Website::new(1, 'base')
93+
)
94+
->withStore(
95+
Store::new(1, 'default')
96+
->withWebsite(1, 1)
97+
)
98+
->withStore(
99+
Store::new(2, 'english')
100+
->withWebsite(1, 1)
101+
)
102+
)
103+
->withDefaultValue('some/other', 'default_one_value')
104+
->withWebsiteValue('base', 'some/other/path_two', 'base_website_value')
105+
->withStoreValue('default', 'some/other/path_three', 'default_store_value')
106+
->withStoreValue('english', 'some/other', null);
107+
108+
$this->assertEquals(
109+
[
110+
'path_two' => 'base_website_value',
111+
'path_three' => 'default_store_value',
112+
],
113+
$config->getValue('some/other', ScopeInterface::SCOPE_STORE, 'default')
114+
);
115+
116+
$this->assertEquals(
117+
[
118+
'path_two' => 'base_website_value'
119+
],
120+
$config->getValue('some/other', ScopeInterface::SCOPE_STORE, 'english')
121+
);
122+
}
123+
124+
#[Test]
125+
public function castsOnlyTruthyValuesInFlagCheck()
126+
{
127+
$config = ScopeConfig::new()
128+
->withDefaultValue('some/flag/one', 'true')
129+
->withDefaultValue('some/flag/two', '1')
130+
->withDefaultValue('some/flag/three', '0')
131+
->withDefaultValue('some/flag/four', 'false')
132+
->withDefaultValue('some/flag/five', '')
133+
;
134+
$this->assertEquals(
135+
[
136+
true,
137+
true,
138+
false,
139+
true,
140+
false
141+
],
142+
[
143+
$config->isSetFlag('some/flag/one'),
144+
$config->isSetFlag('some/flag/two'),
145+
$config->isSetFlag('some/flag/three'),
146+
$config->isSetFlag('some/flag/four'),
147+
$config->isSetFlag('some/flag/five'),
148+
]
149+
);
150+
}
151+
}

tests/Store/StoreManagerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function containsAdminWebsiteByDefault()
2020
{
2121
$this->assertEquals(
2222
[
23-
Website::new(0, 'base')
23+
Website::new(0, 'admin')
2424
],
2525
StoreManager::new()->getWebsites(true)
2626
);
@@ -48,7 +48,7 @@ public function returnsAddedWebsitesGroupedByCode()
4848
{
4949
$this->assertEquals(
5050
[
51-
'base' => Website::new(0, 'base'),
51+
'admin' => Website::new(0, 'admin'),
5252
'two' => Website::new(2, 'two'),
5353
'three' => Website::new(3, 'three'),
5454
'five' => Website::new(5, 'five'),

0 commit comments

Comments
 (0)