createConfig(); $request = $this->createRequest($pathInfo, null); $request->expects(self::once())->method('setPathInfo')->with($expectedPath); $request->expects(self::exactly(3))->method('setParam'); $request->expects(self::exactly(3))->method('setAlias'); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->expects(self::once())->method('setCurrentStore')->with($expectedStoreCode); $plugin = $this->createPlugin($config, $market, $storeManager); $proceedCalled = false; $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), function (HttpRequest $passedRequest) use (&$proceedCalled, $request) { $proceedCalled = true; self::assertSame($request, $passedRequest); return 'proceeded'; }, $request ); self::assertTrue($proceedCalled); } /** * @return mixed[] */ public static function productPathStoreProvider(): array { return [ 'english product path without cookie uses language default' => ['/en/product-a', null, 'en_US', '/product-a'], 'english product path in DE market cookie' => ['/en/product-a', 'en_DE', 'en_DE', '/product-a'], 'german product path without cookie uses language default' => ['/de/product-a', null, 'de_US', '/product-a'], 'german product path in DE market cookie' => ['/de/product-a', 'de_DE', 'de_DE', '/product-a'], 'german customer path uses the same language resolver' => [ '/de/customer/account/login', null, 'de_US', '/customer/account/login', ], ]; } /** * @dataProvider realStoreCodeRedirectProvider */ public function testRedirectsRealStoreCodeUrl(string $pathInfo, string $expectedUrl): void { $config = $this->createConfig(); $request = $this->createRequest($pathInfo, null); $request->method('getRequestUri')->willReturn($pathInfo); $request->method('getBaseUrl')->willReturn(''); $response = $this->createResponse($expectedUrl, 301); $marketCookie = $this->createMock(MarketCookie::class); $marketCookie->expects(self::once())->method('set'); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->expects(self::never())->method('setCurrentStore'); $plugin = $this->createPlugin($config, null, $storeManager, $marketCookie, $response); $proceedCalled = false; $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), function () use (&$proceedCalled) { $proceedCalled = true; }, $request ); self::assertSame($response, $result); self::assertFalse($proceedCalled); } /** * @return mixed[] */ public static function realStoreCodeRedirectProvider(): array { return [ 'english DE store code' => ['/en_DE/product-a', '/en/product-a'], 'german DE store code' => ['/de_DE/product-a', '/de/product-a'], ]; } /** * @dataProvider marketRedirectProvider */ public function testRedirectsMarketSwitchUrl(string $pathInfo, string $market, string $expectedUrl): void { $config = $this->createConfig(); $request = $this->createRequest($pathInfo, $market); $request->method('getRequestUri')->willReturn($pathInfo . '?market=' . $market); $request->method('getBaseUrl')->willReturn(''); $response = $this->createResponse($expectedUrl, 302); $marketCookie = $this->createMock(MarketCookie::class); $marketCookie->expects(self::once())->method('set')->with($market); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->expects(self::never())->method('setCurrentStore'); $plugin = $this->createPlugin($config, null, $storeManager, $marketCookie, $response); $proceedCalled = false; $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), function () use (&$proceedCalled) { $proceedCalled = true; }, $request ); self::assertSame($response, $result); self::assertFalse($proceedCalled); } /** * @return mixed[] */ public static function marketRedirectProvider(): array { return [ 'english URL switches to german DE store' => ['/en/', 'de_DE', '/de/'], 'english URL stays english DE store' => ['/en/', 'en_DE', '/en/'], 'german URL stays german US store' => ['/de/', 'de_US', '/de/'], 'root URL switches to german DE store' => ['/', 'de_DE', '/de/'], 'root URL switches to english DE store' => ['/', 'en_DE', '/en/'], ]; } /** * @dataProvider skippedPathProvider */ public function testSkippedPathsAreNotProcessed(string $pathInfo): void { $config = $this->createConfig(); $request = $this->createRequest($pathInfo, null); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->expects(self::never())->method('setCurrentStore'); $plugin = $this->createPlugin($config, null, $storeManager); $proceedCalled = false; $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), function () use (&$proceedCalled) { $proceedCalled = true; return 'skipped'; }, $request ); self::assertSame('skipped', $result); self::assertTrue($proceedCalled); } public function testInvalidMarketParamDoesNotSetCurrentStore(): void { $config = $this->createConfig(); $request = $this->createRequest('/en/product-a', 'fr_FR'); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->expects(self::never())->method('setCurrentStore'); $plugin = $this->createPlugin($config, null, $storeManager); $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), static function () { return 'invalid-market-proceeded'; }, $request ); self::assertSame('invalid-market-proceeded', $result); } public function testRootPathForcesWebsiteDefaultStore(): void { $config = $this->createConfig(); $request = $this->createRequest('/', null); $request->expects(self::exactly(2))->method('setParam'); $request->expects(self::exactly(2))->method('setAlias'); $defaultStore = $this->createMock(Store::class); $defaultStore->method('isActive')->willReturn(true); $defaultStore->method('getCode')->willReturn('en_US'); $website = $this->createMock(Website::class); $website->method('getDefaultStore')->willReturn($defaultStore); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->method('getWebsite')->willReturn($website); $storeManager->expects(self::once())->method('setCurrentStore')->with('en_US'); $marketCookie = $this->createMock(MarketCookie::class); $marketCookie->expects(self::once())->method('set')->with('en_US'); $plugin = $this->createPlugin($config, 'ja_JP', $storeManager, $marketCookie); $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), static function () { return 'root-proceeded'; }, $request ); self::assertSame('root-proceeded', $result); } public function testUnprefixedContentPathForcesWebsiteDefaultStore(): void { $config = $this->createConfig(); $request = $this->createRequest('/product-a.html', null); $request->expects(self::exactly(2))->method('setParam'); $request->expects(self::exactly(2))->method('setAlias'); $defaultStore = $this->createMock(Store::class); $defaultStore->method('isActive')->willReturn(true); $defaultStore->method('getCode')->willReturn('en_US'); $website = $this->createMock(Website::class); $website->method('getDefaultStore')->willReturn($defaultStore); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->method('getWebsite')->willReturn($website); $storeManager->expects(self::once())->method('setCurrentStore')->with('en_US'); $marketCookie = $this->createMock(MarketCookie::class); $marketCookie->expects(self::once())->method('set')->with('en_US'); $plugin = $this->createPlugin($config, 'de_US', $storeManager, $marketCookie); $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), static function () { return 'unprefixed-product-proceeded'; }, $request ); self::assertSame('unprefixed-product-proceeded', $result); } public function testCustomerAccountPathForcesWebsiteDefaultStoreWithoutLanguageContext(): void { $config = $this->createConfig(); $request = $this->createRequest('/customer/account/login', null); $request->expects(self::exactly(2))->method('setParam'); $request->expects(self::exactly(2))->method('setAlias'); $defaultStore = $this->createMock(Store::class); $defaultStore->method('isActive')->willReturn(true); $defaultStore->method('getCode')->willReturn('en_US'); $website = $this->createMock(Website::class); $website->method('getDefaultStore')->willReturn($defaultStore); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->method('getWebsite')->willReturn($website); $storeManager->expects(self::once())->method('setCurrentStore')->with('en_US'); $marketCookie = $this->createMock(MarketCookie::class); $marketCookie->expects(self::once())->method('set')->with('en_US'); $plugin = $this->createPlugin($config, null, $storeManager, $marketCookie); $result = $plugin->aroundDispatch( $this->createMock(FrontControllerInterface::class), static function () { return 'customer-login-proceeded'; }, $request ); self::assertSame('customer-login-proceeded', $result); } /** * @return mixed[] */ public static function skippedPathProvider(): array { return [ 'static' => ['/static/version123/frontend/file.css'], 'media' => ['/media/catalog/product/a.jpg'], 'rest' => ['/rest/V1/products'], 'graphql' => ['/graphql'], ]; } private function createPlugin( Config $config, ?string $cookieMarket, StoreManagerInterface $storeManager, ?MarketCookie $marketCookie = null, ?HttpResponse $response = null ): LanguageUrlFrontControllerPlugin { $registry = $this->createRegistry(); $scopeStoreManager = $this->createScopeStoreManager(); $languageResolver = new LanguageResolver($registry, $scopeStoreManager); $marketCookie = $marketCookie ?: $this->createMock(MarketCookie::class); $marketCookie->method('get')->willReturn($cookieMarket); $storeCodeMapper = new StoreCodeMapper($registry, $scopeStoreManager); return new LanguageUrlFrontControllerPlugin( $config, $languageResolver, new MarketResolver($marketCookie, $storeCodeMapper), $storeCodeMapper, new RequestPathProcessor($languageResolver), $storeManager, $marketCookie, $response ?: $this->createMock(HttpResponse::class), $this->createMock(LoggerInterface::class), $this->createMock(\Magento\Framework\Locale\ResolverInterface::class), $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class) ); } private function createConfig(): Config { $scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $scopeConfig->method('isSetFlag')->willReturn(true); $scopeConfig->method('getValue')->willReturnCallback(static function (string $path, string $scope) { self::assertSame(\Magento\Store\Model\ScopeInterface::SCOPE_STORE, $scope); $values = [ 'language_url_store_resolver/general/market_cookie_name' => 'market', 'language_url_store_resolver/general/market_cookie_lifetime' => '2592000', ]; return $values[$path] ?? null; }); return new Config($scopeConfig); } private function createRegistry(): StoreViewRegistry { $registry = $this->createMock(StoreViewRegistry::class); $registry->method('isKnownLanguage')->willReturnCallback(static function (string $language): bool { return in_array($language, ['en', 'de'], true); }); $registry->method('isKnownStoreCode')->willReturnCallback(static function (string $storeCode): bool { return in_array($storeCode, ['en_US', 'en_DE', 'de_US', 'de_DE'], true); }); $registry->method('getLanguageByStoreCode')->willReturnCallback(static function (string $storeCode): ?string { $map = [ 'en_US' => 'en', 'en_DE' => 'en', 'de_US' => 'de', 'de_DE' => 'de', ]; return $map[$storeCode] ?? null; }); $registry->method('getDefaultStoreCodeForLanguage')->willReturnCallback( static function (string $language): ?string { $map = [ 'en' => 'en_US', 'de' => 'de_US', ]; return $map[$language] ?? null; } ); $registry->method('isLanguageInWebsite')->willReturnCallback( static fn(string $language, int $websiteId): bool => $websiteId === 1 && in_array($language, ['en', 'de'], true) ); $registry->method('isStoreCodeInWebsite')->willReturnCallback( static fn(string $storeCode, int $websiteId): bool => $websiteId === 1 && in_array($storeCode, ['en_US', 'en_DE', 'de_US', 'de_DE'], true) ); $registry->method('getStoreCodeForLanguageInWebsite')->willReturnCallback( static function (string $language, int $websiteId): ?string { if ($websiteId !== 1) { return null; } return ['en' => 'en_US', 'de' => 'de_US'][$language] ?? null; } ); return $registry; } private function createScopeStoreManager(): StoreManagerInterface { $defaultStore = $this->createMock(Store::class); $defaultStore->method('getCode')->willReturn('default_store'); $website = $this->createMock(Website::class); $website->method('getId')->willReturn(1); $website->method('getDefaultStore')->willReturn($defaultStore); $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->method('getWebsite')->willReturn($website); return $storeManager; } private function createRequest(string $pathInfo, ?string $market): HttpRequest { $request = $this->getMockBuilder(HttpRequest::class) ->disableOriginalConstructor() ->onlyMethods(['getPathInfo', 'getRequestUri', 'getBaseUrl', 'getParam', 'setPathInfo', 'setParam', 'setAlias']) ->getMock(); $request->method('getPathInfo')->willReturn($pathInfo); $request->method('getRequestUri')->willReturn($pathInfo); $request->method('getBaseUrl')->willReturn(''); $request->method('getParam')->with('market')->willReturn($market); return $request; } private function createResponse(string $expectedUrl, int $expectedStatusCode): HttpResponse { $response = $this->getMockBuilder(HttpResponse::class) ->disableOriginalConstructor() ->onlyMethods(['setRedirect']) ->getMock(); $response->expects(self::once()) ->method('setRedirect') ->with($expectedUrl, $expectedStatusCode) ->willReturnSelf(); return $response; } }