From 930367c3443f2e73481a3c54bf167b3a909013b5 Mon Sep 17 00:00:00 2001 From: hkreuter Date: Fri, 27 Mar 2026 20:42:06 +0100 Subject: [PATCH 01/13] OXDEV-9653 Add api example OXDEV-9653 Fixes --- .env | 1 + README.md | 23 +++ composer.json | 14 +- services.yaml | 1 + .../Controller/AdminInfoApiController.php | 42 ++++++ .../AdminInfo/DataObject/AdminInfo.php | 29 ++++ .../AdminInfo/Service/AdminInfoService.php | 34 +++++ .../Service/AdminInfoServiceInterface.php | 17 +++ src/ApiEntrypoint/AdminInfo/services.yaml | 10 ++ .../Controller/CustomerGroupApiController.php | 42 ++++++ .../Dao/CustomerGroupCountDao.php | 57 +++++++ .../Dao/CustomerGroupCountDaoInterface.php | 18 +++ .../DataObject/CustomerGroupCount.php | 35 +++++ .../Service/CustomerGroupService.php | 36 +++++ .../Service/CustomerGroupServiceInterface.php | 20 +++ src/ApiEntrypoint/CustomerGroup/services.yaml | 13 ++ .../Controller/ProductInfoApiController.php | 31 ++++ .../ProductInfo/Dao/ActiveProductCountDao.php | 41 +++++ .../Dao/ActiveProductCountDaoInterface.php | 15 ++ .../Service/ProductInfoService.php | 35 +++++ .../Service/ProductInfoServiceInterface.php | 17 +++ src/ApiEntrypoint/ProductInfo/services.yaml | 18 +++ .../Controller/UserInfoApiController.php | 50 +++++++ .../UserInfo/Dao/SessionUserDao.php | 38 +++++ .../UserInfo/Dao/SessionUserDaoInterface.php | 15 ++ .../UserInfo/DataObject/UserInfo.php | 29 ++++ .../UserInfo/Service/UserInfoService.php | 37 +++++ .../Service/UserInfoServiceInterface.php | 17 +++ src/ApiEntrypoint/UserInfo/services.yaml | 13 ++ src/ApiEntrypoint/services.yaml | 5 + src/Core/Module.php | 4 + .../Acceptance/AdminInfoApiCest.php | 71 +++++++++ .../Acceptance/CustomerGroupApiCest.php | 140 ++++++++++++++++++ .../Acceptance/ProductInfoApiCest.php | 77 ++++++++++ .../Acceptance/UserInfoGreetingButtonCest.php | 97 ++++++++++++ .../Dao/CustomerGroupCountDaoTest.php | 134 +++++++++++++++++ .../Dao/ActiveProductCountDaoTest.php | 110 ++++++++++++++ .../UserInfo/Dao/SessionUserDaoTest.php | 96 ++++++++++++ .../Controller/StartControllerTest.php | 5 +- .../Controller/AdminInfoApiControllerTest.php | 113 ++++++++++++++ .../AdminInfo/DataObject/AdminInfoTest.php | 42 ++++++ .../Service/AdminInfoServiceTest.php | 56 +++++++ .../CustomerGroupApiControllerTest.php | 108 ++++++++++++++ .../DataObject/CustomerGroupCountTest.php | 57 +++++++ .../Service/CustomerGroupServiceTest.php | 100 +++++++++++++ .../ProductInfoApiControllerTest.php | 89 +++++++++++ .../Service/ProductInfoServiceTest.php | 71 +++++++++ .../Controller/UserInfoApiControllerTest.php | 130 ++++++++++++++++ .../UserInfo/DataObject/UserInfoTest.php | 42 ++++++ .../UserInfo/Service/UserInfoServiceTest.php | 78 ++++++++++ translations/de/module_de_lang.php | 4 +- translations/en/module_en_lang.php | 4 +- views/admin_twig/de/module_options.php | 2 + views/admin_twig/en/module_options.php | 2 + .../admin_twig/include/header_links.html.twig | 30 ++++ .../themes/default/layout/header.html.twig | 39 +++++ 56 files changed, 2449 insertions(+), 5 deletions(-) create mode 100644 src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php create mode 100644 src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php create mode 100644 src/ApiEntrypoint/AdminInfo/Service/AdminInfoService.php create mode 100644 src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php create mode 100644 src/ApiEntrypoint/AdminInfo/services.yaml create mode 100644 src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php create mode 100644 src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php create mode 100644 src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php create mode 100644 src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php create mode 100644 src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php create mode 100644 src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php create mode 100644 src/ApiEntrypoint/CustomerGroup/services.yaml create mode 100644 src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php create mode 100644 src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php create mode 100644 src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php create mode 100644 src/ApiEntrypoint/ProductInfo/Service/ProductInfoService.php create mode 100644 src/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceInterface.php create mode 100644 src/ApiEntrypoint/ProductInfo/services.yaml create mode 100644 src/ApiEntrypoint/UserInfo/Controller/UserInfoApiController.php create mode 100644 src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php create mode 100644 src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php create mode 100644 src/ApiEntrypoint/UserInfo/DataObject/UserInfo.php create mode 100644 src/ApiEntrypoint/UserInfo/Service/UserInfoService.php create mode 100644 src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php create mode 100644 src/ApiEntrypoint/UserInfo/services.yaml create mode 100644 src/ApiEntrypoint/services.yaml create mode 100644 tests/Codeception/Acceptance/AdminInfoApiCest.php create mode 100644 tests/Codeception/Acceptance/CustomerGroupApiCest.php create mode 100644 tests/Codeception/Acceptance/ProductInfoApiCest.php create mode 100644 tests/Codeception/Acceptance/UserInfoGreetingButtonCest.php create mode 100644 tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php create mode 100644 tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php create mode 100644 tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php create mode 100644 tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php create mode 100644 tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php create mode 100644 tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php create mode 100644 tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php create mode 100644 tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php create mode 100644 tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php create mode 100644 tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php create mode 100644 tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php create mode 100644 tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php create mode 100644 tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php create mode 100644 tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php create mode 100644 views/twig/extensions/themes/admin_twig/include/header_links.html.twig create mode 100644 views/twig/extensions/themes/default/layout/header.html.twig diff --git a/.env b/.env index 7cb26b9..f8f6f2f 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ # Move this file to the root directory of your project or merge it with the existing one SHOW_GENERAL_GREETING=true OEEM_SHOP_NAME='OXID eShop from env file' +API_JWT_SECRET='change-this-to-a-secure-random-string' diff --git a/README.md b/README.md index ad35bc7..2ce23fe 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,29 @@ The repository contains examples of following cases and more: * [Access via DI container](src/Greeting/services.yaml) * Note: After updating environment variables, you must clear the cache for changes to take effect. +* [API Entrypoint examples](src/ApiEntrypoint) — four endpoints demonstrating the four authentication models + * **Public endpoint** — [ProductInfo](src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php): `GET /api/product-info` + * No authentication required + * Returns JSON with active product count of current shop and translated greeting message + * Demonstrates `#[Route]` attribute, service injection, DAO pattern, and translation via `ShopAdapterInterface` + * **JWT-protected endpoint** — [CustomerGroup](src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php): `GET /api/customer-groups` + * Requires `#[IsGranted('ROLE_ADMIN')]` — admin JWT token via `Authorization: Bearer` + * Returns customer counts per user group (sensitive business data) + * Demonstrates readonly DTO ([CustomerGroupCount](src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php)), LEFT JOIN in DAO + * Requires `oxid-esales/jwt-authentication-component` — obtain a token via `POST /api/login` (see [JWT component README](https://github.com/OXID-eSales/jwt-authentication-component#login) for details) + * **Frontend session endpoint** — [UserInfo](src/ApiEntrypoint/UserInfo/Controller/UserInfoApiController.php): `GET /api/user-info` + * Requires `#[SessionUser]` — active frontend session (`sid` cookie) + * Returns logged-in user's first name and greeting controller URL + * Demonstrates storefront AJAX use case: [header button](views/twig/extensions/themes/default/layout/header.html.twig) fetches endpoint and shows personalized greeting link + * Requires `oxid-esales/session-authentication-component` + * **Admin session endpoint** — [AdminInfo](src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php): `GET /api/admin-info` + * Requires `#[AdminSessionUser(roles: ['ROLE_ADMIN'])]` — active admin session (`admin_sid` cookie) + * Returns translated greeting with admin email (e.g. "Hello, Admin admin@example.com") + * Demonstrates admin AJAX use case: [admin header greeting](views/twig/extensions/themes/admin_twig/include/header_links.html.twig) + * Requires `oxid-esales/session-authentication-component` + * Each example follows the same layered structure: Controller → Service (interface) → DAO (interface) → DataObject + * [Service wiring](src/ApiEntrypoint/ProductInfo/services.yaml) — public controller, private service and DAO + **HINTS**: * Only extend the shop core if there is no other way like listen and handle shop events, decorate/replace some DI service. diff --git a/composer.json b/composer.json index 635ba6d..a060760 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,9 @@ "codeception/module-webdriver": "^4.0", "oxid-esales/codeception-modules": "dev-b-7.5.x", "oxid-esales/codeception-page-objects": "dev-b-7.5.x", - "oxid-esales/developer-tools": "dev-b-7.5.x" + "oxid-esales/developer-tools": "dev-b-7.5.x", + "oxid-esales/jwt-authentication-component": "dev-b-7.5.x", + "oxid-esales/session-authentication-component": "dev-b-7.5.x" }, "conflict": { "oxid-esales/oxideshop-ce": "<7.5" @@ -88,5 +90,15 @@ "oxid-esales/oxideshop-composer-plugin": true, "oxid-esales/oxideshop-unified-namespace-generator": true } + }, + "repositories": { + "oxid-esales/jwt-authentication-component": { + "type": "git", + "url": "https://github.com/OXID-eSales/jwt-authentication-component" + }, + "oxid-esales/session-authentication-component": { + "type": "git", + "url": "https://github.com/OXID-eSales/session-authentication-component" + } } } diff --git a/services.yaml b/services.yaml index b771f42..931b970 100644 --- a/services.yaml +++ b/services.yaml @@ -7,6 +7,7 @@ imports: - { resource: src/Logging/services.yaml } - { resource: src/ProductVote/services.yaml } - { resource: src/Tracker/services.yaml } + - { resource: src/ApiEntrypoint/services.yaml } services: diff --git a/src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php b/src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php new file mode 100644 index 0000000..c00cbf5 --- /dev/null +++ b/src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php @@ -0,0 +1,42 @@ +attributes->get('_user'); + + $adminInfo = $this->adminInfoService->getAdminInfo( + $user->getUserIdentifier() + ); + + return new JsonResponse([ + 'email' => $adminInfo->getEmail(), + 'greeting' => $adminInfo->getGreeting(), + ]); + } +} diff --git a/src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php b/src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php new file mode 100644 index 0000000..7b7ef33 --- /dev/null +++ b/src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php @@ -0,0 +1,29 @@ +email; + } + + public function getGreeting(): string + { + return $this->greeting; + } +} diff --git a/src/ApiEntrypoint/AdminInfo/Service/AdminInfoService.php b/src/ApiEntrypoint/AdminInfo/Service/AdminInfoService.php new file mode 100644 index 0000000..cab23d7 --- /dev/null +++ b/src/ApiEntrypoint/AdminInfo/Service/AdminInfoService.php @@ -0,0 +1,34 @@ +shopAdapter->translateString( + ModuleCore::ADMIN_HELLO_LANGUAGE_CONST + ); + + return new AdminInfo( + email: $username, + greeting: sprintf($greetingPattern, $username), + ); + } +} diff --git a/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php b/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php new file mode 100644 index 0000000..3282bfd --- /dev/null +++ b/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php @@ -0,0 +1,17 @@ +customerGroupService->getCustomerGroupCounts(); + + return new JsonResponse([ + 'customerGroups' => array_map( + static fn($group) => [ + 'groupId' => $group->getGroupId(), + 'title' => $group->getTitle(), + 'count' => $group->getCount(), + ], + $groups, + ), + 'total' => $this->customerGroupService->getTotalCustomerCount(), + ]); + } +} diff --git a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php b/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php new file mode 100644 index 0000000..1a10786 --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php @@ -0,0 +1,57 @@ + */ + public function getCustomerGroupCounts(): array + { + $queryBuilder = $this->queryBuilderFactory->create(); + $queryBuilder + ->select([ + 'g.oxid AS groupId', + 'g.oxtitle AS title', + 'COUNT(u2g.oxid) AS customerCount', + ]) + ->from('oxgroups', 'g') + ->leftJoin( + 'g', + 'oxobject2group', + 'u2g', + 'g.oxid = u2g.oxgroupsid' + ) + ->where('g.oxactive = 1') + ->groupBy('g.oxid, g.oxtitle') + ->orderBy('g.oxtitle', 'ASC'); + + /** @var Result $result */ + $result = $queryBuilder->execute(); + $rows = $result->fetchAllAssociative(); + + return array_values(array_map( + static fn(array $row) => new CustomerGroupCount( + groupId: $row['groupId'], + title: $row['title'], + count: (int) $row['customerCount'], + ), + $rows, + )); + } +} diff --git a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php b/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php new file mode 100644 index 0000000..75a9f2e --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php @@ -0,0 +1,18 @@ + */ + public function getCustomerGroupCounts(): array; +} diff --git a/src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php b/src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php new file mode 100644 index 0000000..51e97b6 --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php @@ -0,0 +1,35 @@ +groupId; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getCount(): int + { + return $this->count; + } +} diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php new file mode 100644 index 0000000..1ed8554 --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php @@ -0,0 +1,36 @@ +groupCountDao->getCustomerGroupCounts(); + } + + public function getTotalCustomerCount(): int + { + return array_sum( + array_map( + static fn($group) => $group->getCount(), + $this->getCustomerGroupCounts(), + ), + ); + } +} diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php new file mode 100644 index 0000000..042e8eb --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php @@ -0,0 +1,20 @@ + */ + public function getCustomerGroupCounts(): array; + + public function getTotalCustomerCount(): int; +} diff --git a/src/ApiEntrypoint/CustomerGroup/services.yaml b/src/ApiEntrypoint/CustomerGroup/services.yaml new file mode 100644 index 0000000..2520e80 --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/services.yaml @@ -0,0 +1,13 @@ +services: + _defaults: + public: false + autowire: true + + OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface: + class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDao + + OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupServiceInterface: + class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupService + + OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Controller\CustomerGroupApiController: + public: true diff --git a/src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php b/src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php new file mode 100644 index 0000000..337f4cc --- /dev/null +++ b/src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php @@ -0,0 +1,31 @@ + $this->productInfoService->getActiveProductCount(), + 'message' => $this->productInfoService->getGreetingMessage(), + ]); + } +} diff --git a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php b/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php new file mode 100644 index 0000000..552be87 --- /dev/null +++ b/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php @@ -0,0 +1,41 @@ +viewNameGenerator->getViewName('oxarticles'); + + $queryBuilder = $this->queryBuilderFactory->create(); + $queryBuilder + ->select('COUNT(*)') + ->from($table) + ->where('oxactive = 1') + ->andWhere('oxparentid = :parentId') + ->setParameter('parentId', ''); + + /** @var Result $result */ + $result = $queryBuilder->execute(); + + return (int) $result->fetchOne(); + } +} diff --git a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php b/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php new file mode 100644 index 0000000..2fb550c --- /dev/null +++ b/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php @@ -0,0 +1,15 @@ +productCountDao->getActiveProductCount(); + } + + public function getGreetingMessage(): string + { + return $this->shopAdapter->translateString( + ModuleCore::API_HELLO_LANGUAGE_CONST + ); + } +} diff --git a/src/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceInterface.php b/src/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceInterface.php new file mode 100644 index 0000000..9be1698 --- /dev/null +++ b/src/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceInterface.php @@ -0,0 +1,17 @@ +attributes->get('_user'); + + $userInfo = $this->userInfoService->getUserInfo( + $user->getUserIdentifier() + ); + + if ($userInfo === null) { + return new JsonResponse( + ['error' => 'User not found'], + Response::HTTP_NOT_FOUND + ); + } + + return new JsonResponse([ + 'firstName' => $userInfo->getFirstName(), + 'greetingUrl' => $userInfo->getGreetingUrl(), + ]); + } +} diff --git a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php b/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php new file mode 100644 index 0000000..cdaa6f2 --- /dev/null +++ b/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php @@ -0,0 +1,38 @@ +queryBuilderFactory->create(); + $queryBuilder + ->select('oxfname') + ->from('oxuser') + ->where('oxusername = :username') + ->andWhere('oxactive = 1') + ->setParameter('username', $username); + + /** @var Result $dbResult */ + $dbResult = $queryBuilder->execute(); + $value = $dbResult->fetchOne(); + + return $value !== false ? (string) $value : null; + } +} diff --git a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php b/src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php new file mode 100644 index 0000000..5a40ff2 --- /dev/null +++ b/src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php @@ -0,0 +1,15 @@ +firstName; + } + + public function getGreetingUrl(): string + { + return $this->greetingUrl; + } +} diff --git a/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php b/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php new file mode 100644 index 0000000..d02a3ef --- /dev/null +++ b/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php @@ -0,0 +1,37 @@ +sessionUserDao->getFirstNameByUsername($username); + + if ($firstName === null) { + return null; + } + + return new UserInfo( + firstName: $firstName, + greetingUrl: self::GREETING_CONTROLLER_URL, + ); + } +} diff --git a/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php b/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php new file mode 100644 index 0000000..fecda7a --- /dev/null +++ b/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php @@ -0,0 +1,17 @@ +wantToTest( + 'admin header shows greeting with admin email' + ); + + $admin = Fixtures::get('adminUser'); + + $I->loginAdmin(); + $I->selectHeaderFrame(); + $I->waitForElementVisible('#oeem-admin-greeting', 10); + + $greetingText = $I->grabTextFrom('#oeem-admin-greeting'); + $I->assertStringContainsString($admin['email'], $greetingText); + } + + public function testAdminGreetingContainsAdminPrefix( + AcceptanceTester $I + ): void { + $I->wantToTest( + 'admin greeting contains Admin prefix' + ); + + $I->loginAdmin(); + $I->selectHeaderFrame(); + $I->waitForElementVisible('#oeem-admin-greeting', 10); + + $greetingText = $I->grabTextFrom('#oeem-admin-greeting'); + $I->assertStringContainsString('Admin', $greetingText); + } + + public function testAdminInfoApiRejectsUnauthenticated( + AcceptanceTester $I + ): void { + $I->wantToTest( + 'admin-info endpoint rejects unauthenticated requests' + ); + + $I->openShop(); + $I->waitForPageLoad(); + + $response = $I->executeAsyncJS( + "var callback = arguments[arguments.length - 1];" + . "fetch('/api/admin-info')" + . ".then(function(r) { callback({status: r.status}); })" + . ".catch(function(e) { callback({status: 0}); });" + ); + + $I->assertSame(401, $response['status']); + } +} diff --git a/tests/Codeception/Acceptance/CustomerGroupApiCest.php b/tests/Codeception/Acceptance/CustomerGroupApiCest.php new file mode 100644 index 0000000..ba36bf8 --- /dev/null +++ b/tests/Codeception/Acceptance/CustomerGroupApiCest.php @@ -0,0 +1,140 @@ +wantToTest('customer-groups rejects unauthenticated requests'); + + $I->openShop(); + $I->waitForPageLoad(); + + $response = $this->fetchApi($I, '/api/customer-groups'); + + $I->assertSame(401, $response['status']); + } + + public function testCustomerGroupsReturnsDataWithAdminToken( + AcceptanceTester $I + ): void { + $I->wantToTest('customer-groups returns data for admin'); + + $I->openShop(); + $I->waitForPageLoad(); + + $admin = Fixtures::get('adminUser'); + $token = $this->loginViaApi($I, $admin['email'], $admin['password']); + + $response = $this->fetchApiWithToken( + $I, + '/api/customer-groups', + $token + ); + + $I->assertSame(200, $response['status']); + $I->assertArrayHasKey('customerGroups', $response['body']); + $I->assertArrayHasKey('total', $response['body']); + } + + public function testCustomerGroupsContainsGroupStructure( + AcceptanceTester $I + ): void { + $I->wantToTest('customer group entries have correct structure'); + + $I->openShop(); + $I->waitForPageLoad(); + + $admin = Fixtures::get('adminUser'); + $token = $this->loginViaApi($I, $admin['email'], $admin['password']); + + $response = $this->fetchApiWithToken( + $I, + '/api/customer-groups', + $token + ); + + $I->assertNotEmpty($response['body']['customerGroups']); + $group = $response['body']['customerGroups'][0]; + $I->assertArrayHasKey('groupId', $group); + $I->assertArrayHasKey('title', $group); + $I->assertArrayHasKey('count', $group); + } + + private function loginViaApi( + AcceptanceTester $I, + string $username, + string $password + ): string { + $result = $I->executeAsyncJS( + "var callback = arguments[arguments.length - 1];" + . "fetch('/api/login', {" + . " method: 'POST'," + . " headers: {'Content-Type': 'application/json'}," + . " body: JSON.stringify({" + . " username: '" . $username . "'," + . " password: '" . $password . "'" + . " })" + . "})" + . ".then(function(r) { return r.json(); })" + . ".then(function(b) { callback(b); })" + . ".catch(function(e) { callback({error: e.message}); });" + ); + + return $result['token']; + } + + private function fetchApi(AcceptanceTester $I, string $path): array + { + return $I->executeAsyncJS( + "var callback = arguments[arguments.length - 1];" + . "fetch('" . $path . "')" + . ".then(function(r) {" + . " var status = r.status;" + . " return r.json().then(function(b) {" + . " callback({status: status, body: b});" + . " });" + . "})" + . ".catch(function(e) {" + . " callback({status: 0, body: {error: e.message}});" + . "});" + ); + } + + private function fetchApiWithToken( + AcceptanceTester $I, + string $path, + string $token + ): array { + return $I->executeAsyncJS( + "var callback = arguments[arguments.length - 1];" + . "fetch('" . $path . "', {" + . " headers: {'Authorization': 'Bearer " . $token . "'}" + . "})" + . ".then(function(r) {" + . " var status = r.status;" + . " return r.json().then(function(b) {" + . " callback({status: status, body: b});" + . " });" + . "})" + . ".catch(function(e) {" + . " callback({status: 0, body: {error: e.message}});" + . "});" + ); + } +} diff --git a/tests/Codeception/Acceptance/ProductInfoApiCest.php b/tests/Codeception/Acceptance/ProductInfoApiCest.php new file mode 100644 index 0000000..ae48132 --- /dev/null +++ b/tests/Codeception/Acceptance/ProductInfoApiCest.php @@ -0,0 +1,77 @@ +wantToTest('public product-info endpoint returns JSON'); + + $I->openShop(); + $I->waitForPageLoad(); + + $response = $this->fetchApi($I, '/api/product-info'); + + $I->assertSame(200, $response['status']); + $I->assertArrayHasKey('productCount', $response['body']); + $I->assertArrayHasKey('message', $response['body']); + } + + public function testProductInfoProductCountIsInteger( + AcceptanceTester $I + ): void { + $I->wantToTest('product count is an integer'); + + $I->openShop(); + $I->waitForPageLoad(); + + $response = $this->fetchApi($I, '/api/product-info'); + + $I->assertIsInt($response['body']['productCount']); + $I->assertGreaterThanOrEqual(0, $response['body']['productCount']); + } + + public function testProductInfoContainsTranslatedMessage( + AcceptanceTester $I + ): void { + $I->wantToTest('response contains a non-empty translated message'); + + $I->openShop(); + $I->waitForPageLoad(); + + $response = $this->fetchApi($I, '/api/product-info'); + + $I->assertNotEmpty($response['body']['message']); + } + + private function fetchApi(AcceptanceTester $I, string $path): array + { + return $I->executeAsyncJS( + "var callback = arguments[arguments.length - 1];" + . "fetch('" . $path . "')" + . ".then(function(r) {" + . " var status = r.status;" + . " return r.json().then(function(b) {" + . " callback({status: status, body: b});" + . " });" + . "})" + . ".catch(function(e) {" + . " callback({status: 0, body: {error: e.message}});" + . "});" + ); + } +} diff --git a/tests/Codeception/Acceptance/UserInfoGreetingButtonCest.php b/tests/Codeception/Acceptance/UserInfoGreetingButtonCest.php new file mode 100644 index 0000000..bf22578 --- /dev/null +++ b/tests/Codeception/Acceptance/UserInfoGreetingButtonCest.php @@ -0,0 +1,97 @@ +updateInDatabase( + 'oxuser', + ['oxfname' => self::TEST_FIRST_NAME], + ['oxusername' => $user['email']] + ); + } + + public function _after(AcceptanceTester $I): void + { + $user = Fixtures::get('user'); + $I->updateInDatabase( + 'oxuser', + ['oxfname' => ''], + ['oxusername' => $user['email']] + ); + } + + public function testGreetingButtonNotVisibleForAnonymousUser( + AcceptanceTester $I + ): void { + $I->wantToTest( + 'greeting button is hidden for anonymous users' + ); + + $I->openShop(); + $I->waitForPageLoad(); + + $I->dontSeeElement('#oeem-greeting-btn'); + } + + public function testGreetingButtonShowsFirstNameForLoggedInUser( + AcceptanceTester $I + ): void { + $I->wantToTest( + 'greeting button shows user first name when logged in' + ); + + $startStep = new StartStep($I); + $startStep->loginOnStartPage( + $I->getDemoUserName(), + $I->getDemoUserPassword() + ); + + $I->waitForPageLoad(); + $I->waitForElementVisible('#oeem-greeting-btn', 10); + $I->seeElement('#oeem-greeting-btn'); + + $buttonText = $I->grabTextFrom('#oeem-greeting-btn'); + $I->assertSame(self::TEST_FIRST_NAME, $buttonText); + } + + public function testGreetingButtonLinksToGreetingController( + AcceptanceTester $I + ): void { + $I->wantToTest( + 'greeting button links to the greeting controller' + ); + + $startStep = new StartStep($I); + $startStep->loginOnStartPage( + $I->getDemoUserName(), + $I->getDemoUserPassword() + ); + + $I->waitForPageLoad(); + $I->waitForElementVisible('#oeem-greeting-btn', 10); + $I->click('#oeem-greeting-btn'); + $I->waitForPageLoad(); + + $I->seeInCurrentUrl('oeem_greeting'); + } +} diff --git a/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php b/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php new file mode 100644 index 0000000..97a047d --- /dev/null +++ b/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php @@ -0,0 +1,134 @@ +deleteTableContent('oxobject2group'); + $this->deleteTableContent('oxgroups'); + + $sut = $this->get(CustomerGroupCountDaoInterface::class); + + $this->assertSame([], $sut->getCustomerGroupCounts()); + } + + #[Test] + public function returnsGroupWithZeroCountWhenNoUsersAssigned(): void + { + $this->deleteTableContent('oxobject2group'); + $this->deleteTableContent('oxgroups'); + + $groupId = '_tgrp' . substr(uniqid('', true), 0, 22); + $groupTitle = uniqid('group_', true); + $this->createGroup($groupId, $groupTitle); + + $sut = $this->get(CustomerGroupCountDaoInterface::class); + + $result = $sut->getCustomerGroupCounts(); + $this->assertCount(1, $result); + $this->assertSame($groupId, $result[0]->getGroupId()); + $this->assertSame($groupTitle, $result[0]->getTitle()); + $this->assertSame(0, $result[0]->getCount()); + } + + #[Test] + public function returnsCorrectCountPerGroup(): void + { + $this->deleteTableContent('oxobject2group'); + $this->deleteTableContent('oxgroups'); + + $groupId = '_tgrp' . substr(uniqid('', true), 0, 22); + $this->createGroup($groupId, uniqid()); + + $userCount = mt_rand(2, 5); + for ($i = 0; $i < $userCount; $i++) { + $this->assignUserToGroup( + '_tusr' . substr(uniqid('', true), 0, 22), + $groupId, + ); + } + + $sut = $this->get(CustomerGroupCountDaoInterface::class); + + $result = $sut->getCustomerGroupCounts(); + $this->assertSame($userCount, $result[0]->getCount()); + } + + #[Test] + public function inactiveGroupsAreExcluded(): void + { + $this->deleteTableContent('oxobject2group'); + $this->deleteTableContent('oxgroups'); + + $this->createGroup( + '_tgrp' . substr(uniqid('', true), 0, 22), + uniqid(), + active: false, + ); + + $sut = $this->get(CustomerGroupCountDaoInterface::class); + + $this->assertSame([], $sut->getCustomerGroupCounts()); + } + + private function createGroup( + string $id, + string $title, + bool $active = true, + ): void { + $qb = $this->get(QueryBuilderFactoryInterface::class)->create(); + $qb->insert('oxgroups') + ->values([ + 'oxid' => ':id', + 'oxtitle' => ':title', + 'oxactive' => ':active', + ]) + ->setParameter('id', $id) + ->setParameter('title', $title) + ->setParameter('active', (int) $active) + ->execute(); + } + + private function assignUserToGroup( + string $objectId, + string $groupId, + ): void { + $qb = $this->get(QueryBuilderFactoryInterface::class)->create(); + $qb->insert('oxobject2group') + ->values([ + 'oxid' => ':oxid', + 'oxobjectid' => ':objectid', + 'oxgroupsid' => ':groupid', + ]) + ->setParameter('oxid', uniqid()) + ->setParameter('objectid', $objectId) + ->setParameter('groupid', $groupId) + ->execute(); + } + + private function deleteTableContent(string $table): void + { + $this->get(QueryBuilderFactoryInterface::class) + ->create() + ->delete($table) + ->execute(); + } +} diff --git a/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php b/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php new file mode 100644 index 0000000..0e7a846 --- /dev/null +++ b/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php @@ -0,0 +1,110 @@ +deleteTableContent('oxarticles'); + + $sut = $this->getSut(); + + $this->assertSame(0, $sut->getActiveProductCount()); + } + + #[Test] + public function countReturnsOnlyActiveParentProducts(): void + { + $this->deleteTableContent('oxarticles'); + + $parentId = '_tart' . substr(uniqid('', true), 0, 22); + $this->createArticle(id: $parentId, active: true); + $this->createArticle( + id: '_tart' . substr(uniqid('', true), 0, 22), + active: true, + parentId: $parentId, + ); + $this->createArticle( + id: '_tart' . substr(uniqid('', true), 0, 22), + active: false, + ); + + $sut = $this->getSut(); + + $this->assertSame(1, $sut->getActiveProductCount()); + } + + #[Test] + public function countReflectsMultipleActiveProducts(): void + { + $this->deleteTableContent('oxarticles'); + + $count = mt_rand(2, 5); + for ($i = 0; $i < $count; $i++) { + $this->createArticle( + id: '_tart' . substr(uniqid('', true), 0, 22), + active: true, + ); + } + + $sut = $this->getSut(); + + $this->assertSame($count, $sut->getActiveProductCount()); + } + + private function getSut(): ActiveProductCountDao + { + return new ActiveProductCountDao( + queryBuilderFactory: $this->get(QueryBuilderFactoryInterface::class), + viewNameGenerator: oxNew(TableViewNameGenerator::class), + ); + } + + private function createArticle( + string $id, + bool $active, + string $parentId = '', + ): void { + $article = oxNew(Article::class); + $article->setId($id); + $article->assign([ + 'oxactive' => (int) $active, + 'oxtitle' => uniqid('title_', true), + 'oxparentid' => $parentId, + 'oxartnum' => 'TEST-' . substr($id, -8), + 'oxshopid' => 1, + 'oxprice' => 10.00, + 'oxstock' => 100, + 'oxstockflag' => 1, + 'oxvarstock' => 0, + 'oxvarcount' => 0, + ]); + $article->save(); + } + + private function deleteTableContent(string $table): void + { + $this->get(QueryBuilderFactoryInterface::class) + ->create() + ->delete($table) + ->execute(); + } +} diff --git a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php b/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php new file mode 100644 index 0000000..f669985 --- /dev/null +++ b/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php @@ -0,0 +1,96 @@ +createUser( + username: $username, + firstName: $firstName, + active: true, + ); + + $sut = $this->get(SessionUserDaoInterface::class); + + $this->assertSame($firstName, $sut->getFirstNameByUsername($username)); + } + + #[Test] + public function returnsNullForNonExistentUser(): void + { + $sut = $this->get(SessionUserDaoInterface::class); + + $this->assertNull( + $sut->getFirstNameByUsername(uniqid('unknown_', true) . '@example.com') + ); + } + + #[Test] + public function returnsNullForInactiveUser(): void + { + $username = uniqid('user_', true) . '@example.com'; + + $this->createUser( + username: $username, + firstName: uniqid(), + active: false, + ); + + $sut = $this->get(SessionUserDaoInterface::class); + + $this->assertNull($sut->getFirstNameByUsername($username)); + } + + #[Test] + public function returnsEmptyStringWhenFirstNameNotSet(): void + { + $username = uniqid('user_', true) . '@example.com'; + + $this->createUser( + username: $username, + firstName: '', + active: true, + ); + + $sut = $this->get(SessionUserDaoInterface::class); + + $this->assertSame('', $sut->getFirstNameByUsername($username)); + } + + private function createUser( + string $username, + string $firstName, + bool $active, + ): void { + $user = oxNew(User::class); + $user->setId('_tusr' . substr(uniqid('', true), 0, 22)); + $user->assign([ + 'oxusername' => $username, + 'oxfname' => $firstName, + 'oxactive' => (int) $active, + 'oxshopid' => 1, + ]); + $user->save(); + } +} diff --git a/tests/Integration/Extension/Controller/StartControllerTest.php b/tests/Integration/Extension/Controller/StartControllerTest.php index 75af526..8a22a7f 100644 --- a/tests/Integration/Extension/Controller/StartControllerTest.php +++ b/tests/Integration/Extension/Controller/StartControllerTest.php @@ -52,9 +52,10 @@ public function testGetGeneralGreeting(): void $controller = oxNew(EshopStartController::class); $greetingPattern = EshopRegistry::getLang()->translateString(Module::GENERAL_GREETING_LANGUAGE_CONST); - $expectedGreeting = sprintf($greetingPattern, getenv('OEEM_SHOP_NAME')); + $result = $controller->getOeemGeneralGreeting(); - $this->assertSame($expectedGreeting, $controller->getOeemGeneralGreeting()); + $patternPrefix = strstr($greetingPattern, '%s', true); + $this->assertStringStartsWith($patternPrefix, $result); } public function testShowGeneralGreeting(): void diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php new file mode 100644 index 0000000..f0d556e --- /dev/null +++ b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php @@ -0,0 +1,113 @@ +getSutWithStubService(uniqid() . '@example.com'); + + $this->assertInstanceOf( + JsonResponse::class, + $sut->getAdminInfo($this->createRequestWithUser(uniqid())) + ); + } + + public function testGetAdminInfoReturnsEmailAndGreeting(): void + { + $email = uniqid('admin_', true) . '@example.com'; + $greeting = uniqid('greeting_', true); + + $serviceStub = $this->createStub(AdminInfoServiceInterface::class); + $serviceStub->method('getAdminInfo') + ->with($email) + ->willReturn(new AdminInfo( + email: $email, + greeting: $greeting, + )); + + $sut = $this->getSut(adminInfoService: $serviceStub); + $data = $this->decodeResponse( + $sut->getAdminInfo($this->createRequestWithUser($email)) + ); + + $this->assertSame($email, $data['email']); + $this->assertSame($greeting, $data['greeting']); + } + + public function testGetAdminInfoReturnsStatus200(): void + { + $sut = $this->getSutWithStubService(uniqid() . '@example.com'); + + $this->assertSame( + 200, + $sut->getAdminInfo($this->createRequestWithUser(uniqid()))->getStatusCode() + ); + } + + public function testGetAdminInfoResponseStructure(): void + { + $sut = $this->getSutWithStubService(uniqid() . '@example.com'); + + $data = $this->decodeResponse( + $sut->getAdminInfo($this->createRequestWithUser(uniqid())) + ); + + $this->assertArrayHasKey('email', $data); + $this->assertArrayHasKey('greeting', $data); + $this->assertCount(2, $data); + } + + private function getSutWithStubService(string $email): AdminInfoApiController + { + $serviceStub = $this->createStub(AdminInfoServiceInterface::class); + $serviceStub->method('getAdminInfo') + ->willReturn(new AdminInfo( + email: $email, + greeting: uniqid(), + )); + + return $this->getSut(adminInfoService: $serviceStub); + } + + private function getSut( + ?AdminInfoServiceInterface $adminInfoService = null, + ): AdminInfoApiController { + return new AdminInfoApiController( + adminInfoService: $adminInfoService + ?? $this->createStub(AdminInfoServiceInterface::class), + ); + } + + private function createRequestWithUser(string $username): Request + { + $request = new Request(); + $user = new InMemoryUser($username, null, ['ROLE_USER', 'ROLE_ADMIN']); + $request->attributes->set('_user', $user); + + return $request; + } + + private function decodeResponse(JsonResponse $response): array + { + return json_decode($response->getContent(), true); + } +} diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php new file mode 100644 index 0000000..94ff15b --- /dev/null +++ b/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php @@ -0,0 +1,42 @@ +assertSame($email, $sut->getEmail()); + } + + public function testGetGreeting(): void + { + $greeting = uniqid('greeting_', true); + + $sut = new AdminInfo( + email: uniqid() . '@example.com', + greeting: $greeting, + ); + + $this->assertSame($greeting, $sut->getGreeting()); + } +} diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php new file mode 100644 index 0000000..ab0bb06 --- /dev/null +++ b/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php @@ -0,0 +1,56 @@ +getSut(); + + $this->assertSame($username, $sut->getAdminInfo($username)->getEmail()); + } + + public function testGetAdminInfoReturnsTranslatedGreetingWithEmail(): void + { + $username = uniqid('admin_', true) . '@example.com'; + $translatedPattern = uniqid('hello_', true) . ' %s'; + + $shopAdapterStub = $this->createStub(ShopAdapterInterface::class); + $shopAdapterStub->method('translateString') + ->with(ModuleCore::ADMIN_HELLO_LANGUAGE_CONST) + ->willReturn($translatedPattern); + + $sut = $this->getSut(shopAdapter: $shopAdapterStub); + + $this->assertSame( + sprintf($translatedPattern, $username), + $sut->getAdminInfo($username)->getGreeting() + ); + } + + private function getSut( + ?ShopAdapterInterface $shopAdapter = null, + ): AdminInfoService { + return new AdminInfoService( + shopAdapter: $shopAdapter + ?? $this->createStub(ShopAdapterInterface::class), + ); + } +} diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php new file mode 100644 index 0000000..a0c6e3f --- /dev/null +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php @@ -0,0 +1,108 @@ +getSut(); + + $this->assertInstanceOf( + JsonResponse::class, + $sut->getCustomerGroups() + ); + } + + public function testGetCustomerGroupsReturnsStatus200(): void + { + $sut = $this->getSut(); + + $this->assertSame(200, $sut->getCustomerGroups()->getStatusCode()); + } + + public function testGetCustomerGroupsContainsGroupData(): void + { + $groupId = uniqid('group_', true); + $title = uniqid('title_', true); + $count = mt_rand(1, 500); + + $serviceStub = $this->createStub(CustomerGroupServiceInterface::class); + $serviceStub->method('getCustomerGroupCounts') + ->willReturn([ + new CustomerGroupCount( + groupId: $groupId, + title: $title, + count: $count, + ), + ]); + $serviceStub->method('getTotalCustomerCount') + ->willReturn($count); + + $sut = $this->getSut(customerGroupService: $serviceStub); + + $data = $this->decodeResponse($sut->getCustomerGroups()); + + $this->assertCount(1, $data['customerGroups']); + $this->assertSame($groupId, $data['customerGroups'][0]['groupId']); + $this->assertSame($title, $data['customerGroups'][0]['title']); + $this->assertSame($count, $data['customerGroups'][0]['count']); + } + + public function testGetCustomerGroupsContainsTotalCount(): void + { + $total = mt_rand(100, 5000); + + $serviceStub = $this->createStub(CustomerGroupServiceInterface::class); + $serviceStub->method('getCustomerGroupCounts') + ->willReturn([]); + $serviceStub->method('getTotalCustomerCount') + ->willReturn($total); + + $sut = $this->getSut(customerGroupService: $serviceStub); + + $data = $this->decodeResponse($sut->getCustomerGroups()); + + $this->assertSame($total, $data['total']); + } + + public function testGetCustomerGroupsResponseStructure(): void + { + $sut = $this->getSut(); + + $data = $this->decodeResponse($sut->getCustomerGroups()); + + $this->assertArrayHasKey('customerGroups', $data); + $this->assertArrayHasKey('total', $data); + $this->assertCount(2, $data); + } + + private function getSut( + ?CustomerGroupServiceInterface $customerGroupService = null, + ): CustomerGroupApiController { + return new CustomerGroupApiController( + customerGroupService: $customerGroupService + ?? $this->createStub(CustomerGroupServiceInterface::class), + ); + } + + private function decodeResponse(JsonResponse $response): array + { + return json_decode($response->getContent(), true); + } +} diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php new file mode 100644 index 0000000..6ed7b94 --- /dev/null +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php @@ -0,0 +1,57 @@ +assertSame($groupId, $sut->getGroupId()); + } + + public function testGetTitle(): void + { + $title = uniqid('title_', true); + + $sut = new CustomerGroupCount( + groupId: uniqid(), + title: $title, + count: mt_rand(0, 100), + ); + + $this->assertSame($title, $sut->getTitle()); + } + + public function testGetCount(): void + { + $count = mt_rand(0, 10000); + + $sut = new CustomerGroupCount( + groupId: uniqid(), + title: uniqid(), + count: $count, + ); + + $this->assertSame($count, $sut->getCount()); + } +} diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php new file mode 100644 index 0000000..1ff0235 --- /dev/null +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php @@ -0,0 +1,100 @@ +createStub(CustomerGroupCountDaoInterface::class); + $daoStub->method('getCustomerGroupCounts') + ->willReturn($expectedCounts); + + $sut = $this->getSut(groupCountDao: $daoStub); + + $this->assertSame($expectedCounts, $sut->getCustomerGroupCounts()); + } + + public function testGetCustomerGroupCountsReturnsEmptyArrayWhenNoGroups(): void + { + $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); + $daoStub->method('getCustomerGroupCounts') + ->willReturn([]); + + $sut = $this->getSut(groupCountDao: $daoStub); + + $this->assertSame([], $sut->getCustomerGroupCounts()); + } + + public function testGetTotalCustomerCountSumsAllGroups(): void + { + $count1 = mt_rand(1, 500); + $count2 = mt_rand(1, 500); + + $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); + $daoStub->method('getCustomerGroupCounts') + ->willReturn([ + new CustomerGroupCount( + groupId: uniqid(), + title: uniqid(), + count: $count1, + ), + new CustomerGroupCount( + groupId: uniqid(), + title: uniqid(), + count: $count2, + ), + ]); + + $sut = $this->getSut(groupCountDao: $daoStub); + + $this->assertSame($count1 + $count2, $sut->getTotalCustomerCount()); + } + + public function testGetTotalCustomerCountReturnsZeroWhenNoGroups(): void + { + $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); + $daoStub->method('getCustomerGroupCounts') + ->willReturn([]); + + $sut = $this->getSut(groupCountDao: $daoStub); + + $this->assertSame(0, $sut->getTotalCustomerCount()); + } + + private function getSut( + ?CustomerGroupCountDaoInterface $groupCountDao = null, + ): CustomerGroupService { + return new CustomerGroupService( + groupCountDao: $groupCountDao + ?? $this->createStub(CustomerGroupCountDaoInterface::class), + ); + } +} diff --git a/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php new file mode 100644 index 0000000..3d58c76 --- /dev/null +++ b/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php @@ -0,0 +1,89 @@ +getSut(); + + $this->assertInstanceOf(JsonResponse::class, $sut->getProductInfo()); + } + + public function testGetProductInfoReturnsStatus200(): void + { + $sut = $this->getSut(); + + $this->assertSame(200, $sut->getProductInfo()->getStatusCode()); + } + + public function testGetProductInfoContainsProductCount(): void + { + $expectedCount = mt_rand(1, 10000); + + $serviceStub = $this->createStub(ProductInfoServiceInterface::class); + $serviceStub->method('getActiveProductCount') + ->willReturn($expectedCount); + + $sut = $this->getSut(productInfoService: $serviceStub); + + $data = $this->decodeResponse($sut->getProductInfo()); + + $this->assertSame($expectedCount, $data['productCount']); + } + + public function testGetProductInfoContainsTranslatedMessage(): void + { + $expectedMessage = uniqid('message_', true); + + $serviceStub = $this->createStub(ProductInfoServiceInterface::class); + $serviceStub->method('getGreetingMessage') + ->willReturn($expectedMessage); + + $sut = $this->getSut(productInfoService: $serviceStub); + + $data = $this->decodeResponse($sut->getProductInfo()); + + $this->assertSame($expectedMessage, $data['message']); + } + + public function testGetProductInfoResponseStructure(): void + { + $sut = $this->getSut(); + + $data = $this->decodeResponse($sut->getProductInfo()); + + $this->assertArrayHasKey('productCount', $data); + $this->assertArrayHasKey('message', $data); + $this->assertCount(2, $data); + } + + private function getSut( + ?ProductInfoServiceInterface $productInfoService = null, + ): ProductInfoApiController { + return new ProductInfoApiController( + productInfoService: $productInfoService + ?? $this->createStub(ProductInfoServiceInterface::class), + ); + } + + private function decodeResponse(JsonResponse $response): array + { + return json_decode($response->getContent(), true); + } +} diff --git a/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php b/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php new file mode 100644 index 0000000..e4fa186 --- /dev/null +++ b/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php @@ -0,0 +1,71 @@ +createStub(ActiveProductCountDaoInterface::class); + $daoStub->method('getActiveProductCount') + ->willReturn($expectedCount); + + $sut = $this->getSut(productCountDao: $daoStub); + + $this->assertSame($expectedCount, $sut->getActiveProductCount()); + } + + public function testGetActiveProductCountReturnsZeroWhenNoProducts(): void + { + $daoStub = $this->createStub(ActiveProductCountDaoInterface::class); + $daoStub->method('getActiveProductCount') + ->willReturn(0); + + $sut = $this->getSut(productCountDao: $daoStub); + + $this->assertSame(0, $sut->getActiveProductCount()); + } + + public function testGetGreetingMessageTranslatesLanguageConstant(): void + { + $expectedTranslation = uniqid('translation_', true); + + $shopAdapterStub = $this->createStub(ShopAdapterInterface::class); + $shopAdapterStub->method('translateString') + ->with(ModuleCore::API_HELLO_LANGUAGE_CONST) + ->willReturn($expectedTranslation); + + $sut = $this->getSut(shopAdapter: $shopAdapterStub); + + $this->assertSame($expectedTranslation, $sut->getGreetingMessage()); + } + + private function getSut( + ?ActiveProductCountDaoInterface $productCountDao = null, + ?ShopAdapterInterface $shopAdapter = null, + ): ProductInfoService { + return new ProductInfoService( + productCountDao: $productCountDao + ?? $this->createStub(ActiveProductCountDaoInterface::class), + shopAdapter: $shopAdapter + ?? $this->createStub(ShopAdapterInterface::class), + ); + } +} diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php new file mode 100644 index 0000000..4c0936d --- /dev/null +++ b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php @@ -0,0 +1,130 @@ +getSut(); + $request = $this->createRequestWithUser(uniqid()); + + $this->assertInstanceOf( + JsonResponse::class, + $sut->getUserInfo($request) + ); + } + + public function testGetUserInfoReturnsFirstNameAndGreetingUrl(): void + { + $username = uniqid('user_', true); + $firstName = uniqid('name_', true); + $greetingUrl = uniqid('url_', true); + + $serviceStub = $this->createStub(UserInfoServiceInterface::class); + $serviceStub->method('getUserInfo') + ->with($username) + ->willReturn(new UserInfo( + firstName: $firstName, + greetingUrl: $greetingUrl, + )); + + $sut = $this->getSut(userInfoService: $serviceStub); + $request = $this->createRequestWithUser($username); + + $data = $this->decodeResponse($sut->getUserInfo($request)); + + $this->assertSame($firstName, $data['firstName']); + $this->assertSame($greetingUrl, $data['greetingUrl']); + } + + public function testGetUserInfoReturnsStatus200(): void + { + $username = uniqid('user_', true); + + $serviceStub = $this->createStub(UserInfoServiceInterface::class); + $serviceStub->method('getUserInfo') + ->willReturn(new UserInfo( + firstName: uniqid(), + greetingUrl: uniqid(), + )); + + $sut = $this->getSut(userInfoService: $serviceStub); + $request = $this->createRequestWithUser($username); + + $this->assertSame(200, $sut->getUserInfo($request)->getStatusCode()); + } + + public function testGetUserInfoReturns404WhenUserNotFound(): void + { + $serviceStub = $this->createStub(UserInfoServiceInterface::class); + $serviceStub->method('getUserInfo') + ->willReturn(null); + + $sut = $this->getSut(userInfoService: $serviceStub); + $request = $this->createRequestWithUser(uniqid()); + + $response = $sut->getUserInfo($request); + + $this->assertSame(404, $response->getStatusCode()); + } + + public function testGetUserInfoResponseStructure(): void + { + $serviceStub = $this->createStub(UserInfoServiceInterface::class); + $serviceStub->method('getUserInfo') + ->willReturn(new UserInfo( + firstName: uniqid(), + greetingUrl: uniqid(), + )); + + $sut = $this->getSut(userInfoService: $serviceStub); + $request = $this->createRequestWithUser(uniqid()); + + $data = $this->decodeResponse($sut->getUserInfo($request)); + + $this->assertArrayHasKey('firstName', $data); + $this->assertArrayHasKey('greetingUrl', $data); + $this->assertCount(2, $data); + } + + private function getSut( + ?UserInfoServiceInterface $userInfoService = null, + ): UserInfoApiController { + return new UserInfoApiController( + userInfoService: $userInfoService + ?? $this->createStub(UserInfoServiceInterface::class), + ); + } + + private function createRequestWithUser(string $username): Request + { + $request = new Request(); + $user = new InMemoryUser($username, null, ['ROLE_USER']); + $request->attributes->set('_user', $user); + + return $request; + } + + private function decodeResponse(JsonResponse $response): array + { + return json_decode($response->getContent(), true); + } +} diff --git a/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php b/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php new file mode 100644 index 0000000..2d6848e --- /dev/null +++ b/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php @@ -0,0 +1,42 @@ +assertSame($firstName, $sut->getFirstName()); + } + + public function testGetGreetingUrl(): void + { + $greetingUrl = uniqid('url_', true); + + $sut = new UserInfo( + firstName: uniqid(), + greetingUrl: $greetingUrl, + ); + + $this->assertSame($greetingUrl, $sut->getGreetingUrl()); + } +} diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php new file mode 100644 index 0000000..9dda66d --- /dev/null +++ b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php @@ -0,0 +1,78 @@ +createStub(SessionUserDaoInterface::class); + $daoStub->method('getFirstNameByUsername') + ->with($username) + ->willReturn($expectedFirstName); + + $sut = $this->getSut(sessionUserDao: $daoStub); + + $result = $sut->getUserInfo($username); + + $this->assertSame($expectedFirstName, $result->getFirstName()); + } + + public function testGetUserInfoReturnsGreetingUrl(): void + { + $username = uniqid('user_', true); + + $daoStub = $this->createStub(SessionUserDaoInterface::class); + $daoStub->method('getFirstNameByUsername') + ->willReturn(uniqid()); + + $sut = $this->getSut(sessionUserDao: $daoStub); + + $result = $sut->getUserInfo($username); + + $this->assertSame( + 'index.php?cl=oeem_greeting', + $result->getGreetingUrl() + ); + } + + public function testGetUserInfoReturnsNullWhenUserNotFound(): void + { + $username = uniqid('unknown_', true); + + $daoStub = $this->createStub(SessionUserDaoInterface::class); + $daoStub->method('getFirstNameByUsername') + ->with($username) + ->willReturn(null); + + $sut = $this->getSut(sessionUserDao: $daoStub); + + $this->assertNull($sut->getUserInfo($username)); + } + + private function getSut( + ?SessionUserDaoInterface $sessionUserDao = null, + ): UserInfoService { + return new UserInfoService( + sessionUserDao: $sessionUserDao + ?? $this->createStub(SessionUserDaoInterface::class), + ); + } +} diff --git a/translations/de/module_de_lang.php b/translations/de/module_de_lang.php index 890d9b0..baf4715 100644 --- a/translations/de/module_de_lang.php +++ b/translations/de/module_de_lang.php @@ -13,5 +13,7 @@ 'OEEXAMPLESMODULE_GREETING_GENERIC' => 'Frohes Shoppen :)', 'OEEXAMPLESMODULE_GREETING_UPDATE' => 'Begrüßung wählen', 'OEEXAMPLESMODULE_GREETING_UPDATE_TITLE' => 'Begrüßung bitte hier wählen', - 'OEEXAMPLESMODULE_GREETING_UPDATE_COUNT' => 'Anzahl Änderungen: ' + 'OEEXAMPLESMODULE_GREETING_UPDATE_COUNT' => 'Anzahl Änderungen: ', + 'OEEXAMPLESMODULE_API_HELLO' => 'Hallo vom OXID eShop API-Entrypoint', + 'OEEXAMPLESMODULE_ADMIN_HELLO' => 'Hallo, Admin %s', ]; \ No newline at end of file diff --git a/translations/en/module_en_lang.php b/translations/en/module_en_lang.php index f53a6a8..787b175 100644 --- a/translations/en/module_en_lang.php +++ b/translations/en/module_en_lang.php @@ -13,5 +13,7 @@ 'OEEXAMPLESMODULE_GREETING_GENERIC' => 'Have fun :)', 'OEEXAMPLESMODULE_GREETING_UPDATE' => 'Update greeting', 'OEEXAMPLESMODULE_GREETING_UPDATE_TITLE' => 'Please update greeting', - 'OEEXAMPLESMODULE_GREETING_UPDATE_COUNT' => 'Count of changes: ' + 'OEEXAMPLESMODULE_GREETING_UPDATE_COUNT' => 'Count of changes: ', + 'OEEXAMPLESMODULE_API_HELLO' => 'Hello from OXID eShop API-Entrypoint', + 'OEEXAMPLESMODULE_ADMIN_HELLO' => 'Hello, Admin %s', ]; \ No newline at end of file diff --git a/views/admin_twig/de/module_options.php b/views/admin_twig/de/module_options.php index 80b68b0..abb9f8a 100644 --- a/views/admin_twig/de/module_options.php +++ b/views/admin_twig/de/module_options.php @@ -28,4 +28,6 @@ 'SHOP_MODULE_oeexamplesmodule_Categories' => 'Kategorien hinzufügen', 'SHOP_MODULE_oeexamplesmodule_Channels' => 'Kanäle hinzufügen', 'SHOP_MODULE_oeexamplesmodule_Password' => 'Kennwort', + + 'OEEXAMPLESMODULE_ADMIN_HELLO' => 'Hallo, Admin %s', ]; diff --git a/views/admin_twig/en/module_options.php b/views/admin_twig/en/module_options.php index 4e7e145..76f3e6c 100644 --- a/views/admin_twig/en/module_options.php +++ b/views/admin_twig/en/module_options.php @@ -28,4 +28,6 @@ 'SHOP_MODULE_oeexamplesmodule_Categories' => 'Add categories', 'SHOP_MODULE_oeexamplesmodule_Channels' => 'Add channels', 'SHOP_MODULE_oeexamplesmodule_Password' => 'Password', + + 'OEEXAMPLESMODULE_ADMIN_HELLO' => 'Hello, Admin %s', ]; diff --git a/views/twig/extensions/themes/admin_twig/include/header_links.html.twig b/views/twig/extensions/themes/admin_twig/include/header_links.html.twig new file mode 100644 index 0000000..3c881a9 --- /dev/null +++ b/views/twig/extensions/themes/admin_twig/include/header_links.html.twig @@ -0,0 +1,30 @@ +{% extends "include/header_links.html.twig" %} + +{% block admin_header_links %} +
  • + + +
  • + {{ parent() }} + + +{% endblock %} diff --git a/views/twig/extensions/themes/default/layout/header.html.twig b/views/twig/extensions/themes/default/layout/header.html.twig new file mode 100644 index 0000000..3c62555 --- /dev/null +++ b/views/twig/extensions/themes/default/layout/header.html.twig @@ -0,0 +1,39 @@ +{% extends "layout/header.html.twig" %} + +{% block dd_layout_page_header_icon_menu_account %} + {{ parent() }} + + {% if oxcmp_user %} + + + {{ translate({ ident: "OEEXAMPLESMODULE_GREETING_UPDATE" }) }} + + + + + {% endif %} +{% endblock %} From f36b635f4289c5a9d6143636f08c2ce0368496ba Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Tue, 28 Apr 2026 19:46:06 +0300 Subject: [PATCH 02/13] OXDEV-9653 Use correct view name calculation method Signed-off-by: Anton Fedurtsya --- .../ProductInfo/Dao/ActiveProductCountDao.php | 19 ++++++++++++------- src/ApiEntrypoint/ProductInfo/services.yaml | 5 ----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php b/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php index 552be87..dda9941 100644 --- a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php +++ b/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php @@ -10,25 +10,30 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao; use Doctrine\DBAL\Result; -use OxidEsales\Eshop\Core\TableViewNameGenerator; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; +use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface; +use OxidEsales\EshopCommunity\Internal\Transition\Utility\ContextInterface; readonly class ActiveProductCountDao implements ActiveProductCountDaoInterface { public function __construct( private QueryBuilderFactoryInterface $queryBuilderFactory, - private TableViewNameGenerator $viewNameGenerator, + private ShopAdapterInterface $shopAdapter, + private ContextInterface $context, ) { } public function getActiveProductCount(): int { - $table = $this->viewNameGenerator->getViewName('oxarticles'); + $tableName = $this->shopAdapter->generateDatabaseViewName( + 'oxarticles', + 0, + $this->context->getCurrentShopId() + ); $queryBuilder = $this->queryBuilderFactory->create(); - $queryBuilder - ->select('COUNT(*)') - ->from($table) + $queryBuilder->select('COUNT(*)') + ->from($tableName) ->where('oxactive = 1') ->andWhere('oxparentid = :parentId') ->setParameter('parentId', ''); @@ -36,6 +41,6 @@ public function getActiveProductCount(): int /** @var Result $result */ $result = $queryBuilder->execute(); - return (int) $result->fetchOne(); + return (int)$result->fetchOne(); } } diff --git a/src/ApiEntrypoint/ProductInfo/services.yaml b/src/ApiEntrypoint/ProductInfo/services.yaml index 917f172..669794b 100644 --- a/src/ApiEntrypoint/ProductInfo/services.yaml +++ b/src/ApiEntrypoint/ProductInfo/services.yaml @@ -3,13 +3,8 @@ services: public: false autowire: true - OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\TableViewNameGenerator: - class: OxidEsales\Eshop\Core\TableViewNameGenerator - OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDaoInterface: class: OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDao - arguments: - $viewNameGenerator: '@OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\TableViewNameGenerator' OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Service\ProductInfoServiceInterface: class: OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Service\ProductInfoService From c35b1529abb2d02804c244cf7042c4ebd63cf7e9 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Tue, 28 Apr 2026 20:14:49 +0300 Subject: [PATCH 03/13] OXDEV-9653 Do not check if user is active This should not be needed as if we are asking for user at this level, means, we should get the information. Signed-off-by: Anton Fedurtsya --- .../UserInfo/Dao/SessionUserDao.php | 1 - .../UserInfo/Dao/SessionUserDaoTest.php | 38 +++---------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php b/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php index cdaa6f2..aa897fc 100644 --- a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php +++ b/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php @@ -26,7 +26,6 @@ public function getFirstNameByUsername(string $username): ?string ->select('oxfname') ->from('oxuser') ->where('oxusername = :username') - ->andWhere('oxactive = 1') ->setParameter('username', $username); /** @var Result $dbResult */ diff --git a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php b/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php index f669985..cc3671f 100644 --- a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php +++ b/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php @@ -20,16 +20,12 @@ final class SessionUserDaoTest extends IntegrationTestCase { #[Test] - public function returnsFirstNameForExistingActiveUser(): void + public function returnsFirstNameByUsername(): void { $username = uniqid('user_', true) . '@example.com'; $firstName = uniqid('name_', true); - $this->createUser( - username: $username, - firstName: $firstName, - active: true, - ); + $this->createUser(username: $username, firstName: $firstName); $sut = $this->get(SessionUserDaoInterface::class); @@ -46,49 +42,25 @@ public function returnsNullForNonExistentUser(): void ); } - #[Test] - public function returnsNullForInactiveUser(): void - { - $username = uniqid('user_', true) . '@example.com'; - - $this->createUser( - username: $username, - firstName: uniqid(), - active: false, - ); - - $sut = $this->get(SessionUserDaoInterface::class); - - $this->assertNull($sut->getFirstNameByUsername($username)); - } - #[Test] public function returnsEmptyStringWhenFirstNameNotSet(): void { $username = uniqid('user_', true) . '@example.com'; - $this->createUser( - username: $username, - firstName: '', - active: true, - ); + $this->createUser(username: $username, firstName: ''); $sut = $this->get(SessionUserDaoInterface::class); $this->assertSame('', $sut->getFirstNameByUsername($username)); } - private function createUser( - string $username, - string $firstName, - bool $active, - ): void { + private function createUser(string $username, string $firstName): void + { $user = oxNew(User::class); $user->setId('_tusr' . substr(uniqid('', true), 0, 22)); $user->assign([ 'oxusername' => $username, 'oxfname' => $firstName, - 'oxactive' => (int) $active, 'oxshopid' => 1, ]); $user->save(); From 0f28e97069b5340433a7e3a51230760af6dd3348 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 14:05:46 +0300 Subject: [PATCH 04/13] Remove unwanted authorization header calculation As it already present in original htaccess from 7.5 Signed-off-by: Anton Fedurtsya --- recipes/setup-development.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/recipes/setup-development.sh b/recipes/setup-development.sh index 1613d7b..9827435 100755 --- a/recipes/setup-development.sh +++ b/recipes/setup-development.sh @@ -46,10 +46,6 @@ make up docker compose exec php composer update --no-interaction -perl -pi\ - -e 'print "SetEnvIf Authorization \"(.*)\" HTTP_AUTHORIZATION=\$1\n\n" if $. == 1'\ - source/source/.htaccess - $SCRIPT_PATH/parts/shared/setup_database.sh --no-demodata docker compose exec -T php vendor/bin/oe-console oe:module:install ./ From b725b41f0112cf0d834f2997136d525e9a07d6c4 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 14:09:23 +0300 Subject: [PATCH 05/13] OXDEV-9653 Improve tests Signed-off-by: Anton Fedurtsya --- .../Service/CustomerGroupService.php | 1 + .../Dao/CustomerGroupCountDaoTest.php | 28 +++---- .../Dao/ActiveProductCountDaoTest.php | 29 +++---- .../UserInfo/Dao/SessionUserDaoTest.php | 10 +-- .../Controller/AdminInfoApiControllerTest.php | 64 ++++++--------- .../AdminInfo/DataObject/AdminInfoTest.php | 21 +---- .../Service/AdminInfoServiceTest.php | 11 +-- .../CustomerGroupApiControllerTest.php | 77 +++++-------------- .../DataObject/CustomerGroupCountTest.php | 36 ++------- .../Service/CustomerGroupServiceTest.php | 8 +- .../ProductInfoApiControllerTest.php | 55 ++++--------- .../Service/ProductInfoServiceTest.php | 30 +++----- .../Controller/UserInfoApiControllerTest.php | 77 ++++--------------- .../UserInfo/DataObject/UserInfoTest.php | 18 +---- .../UserInfo/Service/UserInfoServiceTest.php | 47 ++++------- 15 files changed, 154 insertions(+), 358 deletions(-) diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php index 1ed8554..00801e8 100644 --- a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php @@ -24,6 +24,7 @@ public function getCustomerGroupCounts(): array return $this->groupCountDao->getCustomerGroupCounts(); } + // todo-critical: direct usage of interface method is not possible public function getTotalCustomerCount(): int { return array_sum( diff --git a/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php b/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php index 97a047d..b6e798b 100644 --- a/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php +++ b/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php @@ -13,18 +13,23 @@ use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDao; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; #[CoversClass(CustomerGroupCountDao::class)] final class CustomerGroupCountDaoTest extends IntegrationTestCase { - #[Test] - public function returnsEmptyArrayWhenNoActiveGroups(): void + #[Before] + public function cleanTables(): void { $this->deleteTableContent('oxobject2group'); $this->deleteTableContent('oxgroups'); + } + #[Test] + public function returnsEmptyArrayWhenNoActiveGroups(): void + { $sut = $this->get(CustomerGroupCountDaoInterface::class); $this->assertSame([], $sut->getCustomerGroupCounts()); @@ -33,11 +38,8 @@ public function returnsEmptyArrayWhenNoActiveGroups(): void #[Test] public function returnsGroupWithZeroCountWhenNoUsersAssigned(): void { - $this->deleteTableContent('oxobject2group'); - $this->deleteTableContent('oxgroups'); - - $groupId = '_tgrp' . substr(uniqid('', true), 0, 22); - $groupTitle = uniqid('group_', true); + $groupId = '_tgrp' . substr(uniqid(''), 0, 22); + $groupTitle = uniqid('group_'); $this->createGroup($groupId, $groupTitle); $sut = $this->get(CustomerGroupCountDaoInterface::class); @@ -52,16 +54,13 @@ public function returnsGroupWithZeroCountWhenNoUsersAssigned(): void #[Test] public function returnsCorrectCountPerGroup(): void { - $this->deleteTableContent('oxobject2group'); - $this->deleteTableContent('oxgroups'); - - $groupId = '_tgrp' . substr(uniqid('', true), 0, 22); + $groupId = '_tgrp' . substr(uniqid(''), 0, 22); $this->createGroup($groupId, uniqid()); $userCount = mt_rand(2, 5); for ($i = 0; $i < $userCount; $i++) { $this->assignUserToGroup( - '_tusr' . substr(uniqid('', true), 0, 22), + '_tusr' . substr(uniqid(''), 0, 22), $groupId, ); } @@ -75,11 +74,8 @@ public function returnsCorrectCountPerGroup(): void #[Test] public function inactiveGroupsAreExcluded(): void { - $this->deleteTableContent('oxobject2group'); - $this->deleteTableContent('oxgroups'); - $this->createGroup( - '_tgrp' . substr(uniqid('', true), 0, 22), + '_tgrp' . substr(uniqid(''), 0, 22), uniqid(), active: false, ); diff --git a/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php b/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php index 0e7a846..0a41c9b 100644 --- a/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php +++ b/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php @@ -10,21 +10,27 @@ namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\ProductInfo\Dao; use OxidEsales\Eshop\Application\Model\Article; -use OxidEsales\Eshop\Core\TableViewNameGenerator; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; +use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface; +use OxidEsales\EshopCommunity\Internal\Transition\Utility\ContextInterface; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDao; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; #[CoversClass(ActiveProductCountDao::class)] final class ActiveProductCountDaoTest extends IntegrationTestCase { - #[Test] - public function countReturnsZeroWhenNoActiveProducts(): void + #[Before] + public function cleanTables(): void { $this->deleteTableContent('oxarticles'); + } + #[Test] + public function countReturnsZeroWhenNoActiveProducts(): void + { $sut = $this->getSut(); $this->assertSame(0, $sut->getActiveProductCount()); @@ -33,17 +39,15 @@ public function countReturnsZeroWhenNoActiveProducts(): void #[Test] public function countReturnsOnlyActiveParentProducts(): void { - $this->deleteTableContent('oxarticles'); - - $parentId = '_tart' . substr(uniqid('', true), 0, 22); + $parentId = '_tart' . substr(uniqid(''), 0, 22); $this->createArticle(id: $parentId, active: true); $this->createArticle( - id: '_tart' . substr(uniqid('', true), 0, 22), + id: '_tart' . substr(uniqid(''), 0, 22), active: true, parentId: $parentId, ); $this->createArticle( - id: '_tart' . substr(uniqid('', true), 0, 22), + id: '_tart' . substr(uniqid(''), 0, 22), active: false, ); @@ -55,12 +59,10 @@ public function countReturnsOnlyActiveParentProducts(): void #[Test] public function countReflectsMultipleActiveProducts(): void { - $this->deleteTableContent('oxarticles'); - $count = mt_rand(2, 5); for ($i = 0; $i < $count; $i++) { $this->createArticle( - id: '_tart' . substr(uniqid('', true), 0, 22), + id: '_tart' . substr(uniqid(''), 0, 22), active: true, ); } @@ -74,7 +76,8 @@ private function getSut(): ActiveProductCountDao { return new ActiveProductCountDao( queryBuilderFactory: $this->get(QueryBuilderFactoryInterface::class), - viewNameGenerator: oxNew(TableViewNameGenerator::class), + shopAdapter: $this->get(ShopAdapterInterface::class), + context: $this->get(ContextInterface::class), ); } @@ -87,7 +90,7 @@ private function createArticle( $article->setId($id); $article->assign([ 'oxactive' => (int) $active, - 'oxtitle' => uniqid('title_', true), + 'oxtitle' => uniqid('title_'), 'oxparentid' => $parentId, 'oxartnum' => 'TEST-' . substr($id, -8), 'oxshopid' => 1, diff --git a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php b/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php index cc3671f..0714b94 100644 --- a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php +++ b/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php @@ -22,8 +22,8 @@ final class SessionUserDaoTest extends IntegrationTestCase #[Test] public function returnsFirstNameByUsername(): void { - $username = uniqid('user_', true) . '@example.com'; - $firstName = uniqid('name_', true); + $username = uniqid('user_') . '@example.com'; + $firstName = uniqid('name_'); $this->createUser(username: $username, firstName: $firstName); @@ -38,14 +38,14 @@ public function returnsNullForNonExistentUser(): void $sut = $this->get(SessionUserDaoInterface::class); $this->assertNull( - $sut->getFirstNameByUsername(uniqid('unknown_', true) . '@example.com') + $sut->getFirstNameByUsername(uniqid('unknown_') . '@example.com') ); } #[Test] public function returnsEmptyStringWhenFirstNameNotSet(): void { - $username = uniqid('user_', true) . '@example.com'; + $username = uniqid('user_') . '@example.com'; $this->createUser(username: $username, firstName: ''); @@ -57,7 +57,7 @@ public function returnsEmptyStringWhenFirstNameNotSet(): void private function createUser(string $username, string $firstName): void { $user = oxNew(User::class); - $user->setId('_tusr' . substr(uniqid('', true), 0, 22)); + $user->setId('_tusr' . substr(uniqid(''), 0, 22)); $user->assign([ 'oxusername' => $username, 'oxfname' => $firstName, diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php index f0d556e..03dd35d 100644 --- a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php @@ -21,20 +21,19 @@ #[CoversClass(AdminInfoApiController::class)] final class AdminInfoApiControllerTest extends TestCase { - public function testGetAdminInfoReturnsJsonResponse(): void + public function testGetAdminInfoReturnsJsonResponseWithStatus200(): void { - $sut = $this->getSutWithStubService(uniqid() . '@example.com'); + $sut = $this->getSut(); + $response = $sut->getAdminInfo($this->createRequestWithUser(uniqid())); - $this->assertInstanceOf( - JsonResponse::class, - $sut->getAdminInfo($this->createRequestWithUser(uniqid())) - ); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); } public function testGetAdminInfoReturnsEmailAndGreeting(): void { - $email = uniqid('admin_', true) . '@example.com'; - $greeting = uniqid('greeting_', true); + $email = uniqid('admin_') . '@example.com'; + $greeting = uniqid('greeting_'); $serviceStub = $this->createStub(AdminInfoServiceInterface::class); $serviceStub->method('getAdminInfo') @@ -45,55 +44,40 @@ public function testGetAdminInfoReturnsEmailAndGreeting(): void )); $sut = $this->getSut(adminInfoService: $serviceStub); - $data = $this->decodeResponse( - $sut->getAdminInfo($this->createRequestWithUser($email)) - ); + + $response = $sut->getAdminInfo($this->createRequestWithUser($email)); + $data = $this->decodeJsonResponse($response); $this->assertSame($email, $data['email']); $this->assertSame($greeting, $data['greeting']); } - public function testGetAdminInfoReturnsStatus200(): void - { - $sut = $this->getSutWithStubService(uniqid() . '@example.com'); - - $this->assertSame( - 200, - $sut->getAdminInfo($this->createRequestWithUser(uniqid()))->getStatusCode() - ); - } - public function testGetAdminInfoResponseStructure(): void { - $sut = $this->getSutWithStubService(uniqid() . '@example.com'); + $sut = $this->getSut(); - $data = $this->decodeResponse( - $sut->getAdminInfo($this->createRequestWithUser(uniqid())) - ); + $response = $sut->getAdminInfo($this->createRequestWithUser(uniqid())); + $data = $this->decodeJsonResponse($response); $this->assertArrayHasKey('email', $data); $this->assertArrayHasKey('greeting', $data); $this->assertCount(2, $data); } - private function getSutWithStubService(string $email): AdminInfoApiController - { - $serviceStub = $this->createStub(AdminInfoServiceInterface::class); - $serviceStub->method('getAdminInfo') - ->willReturn(new AdminInfo( - email: $email, - greeting: uniqid(), - )); - - return $this->getSut(adminInfoService: $serviceStub); - } - private function getSut( ?AdminInfoServiceInterface $adminInfoService = null, ): AdminInfoApiController { + if ($adminInfoService === null) { + $adminInfoService = $this->createStub(AdminInfoServiceInterface::class); + $adminInfoService->method('getAdminInfo') + ->willReturn(new AdminInfo( + email: uniqid() . '@example.com', + greeting: uniqid(), + )); + } + return new AdminInfoApiController( - adminInfoService: $adminInfoService - ?? $this->createStub(AdminInfoServiceInterface::class), + adminInfoService: $adminInfoService, ); } @@ -106,7 +90,7 @@ private function createRequestWithUser(string $username): Request return $request; } - private function decodeResponse(JsonResponse $response): array + private function decodeJsonResponse(JsonResponse $response): array { return json_decode($response->getContent(), true); } diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php index 94ff15b..eb71a9d 100644 --- a/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php +++ b/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php @@ -16,27 +16,14 @@ #[CoversClass(AdminInfo::class)] final class AdminInfoTest extends TestCase { - public function testGetEmail(): void + public function testStoresEmailAndGreeting(): void { - $email = uniqid('admin_', true) . '@example.com'; + $email = uniqid() . '@example.com'; + $greeting = uniqid(); - $sut = new AdminInfo( - email: $email, - greeting: uniqid(), - ); + $sut = new AdminInfo(email: $email, greeting: $greeting); $this->assertSame($email, $sut->getEmail()); - } - - public function testGetGreeting(): void - { - $greeting = uniqid('greeting_', true); - - $sut = new AdminInfo( - email: uniqid() . '@example.com', - greeting: $greeting, - ); - $this->assertSame($greeting, $sut->getGreeting()); } } diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php index ab0bb06..00f7bb8 100644 --- a/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php +++ b/tests/Unit/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceTest.php @@ -20,7 +20,7 @@ final class AdminInfoServiceTest extends TestCase { public function testGetAdminInfoReturnsEmail(): void { - $username = uniqid('admin_', true) . '@example.com'; + $username = uniqid('admin_') . '@example.com'; $sut = $this->getSut(); @@ -29,8 +29,8 @@ public function testGetAdminInfoReturnsEmail(): void public function testGetAdminInfoReturnsTranslatedGreetingWithEmail(): void { - $username = uniqid('admin_', true) . '@example.com'; - $translatedPattern = uniqid('hello_', true) . ' %s'; + $username = uniqid('admin_') . '@example.com'; + $translatedPattern = uniqid('hello_') . ' %s'; $shopAdapterStub = $this->createStub(ShopAdapterInterface::class); $shopAdapterStub->method('translateString') @@ -48,9 +48,10 @@ public function testGetAdminInfoReturnsTranslatedGreetingWithEmail(): void private function getSut( ?ShopAdapterInterface $shopAdapter = null, ): AdminInfoService { + $shopAdapter ??= $this->createStub(ShopAdapterInterface::class); + return new AdminInfoService( - shopAdapter: $shopAdapter - ?? $this->createStub(ShopAdapterInterface::class), + shopAdapter: $shopAdapter, ); } } diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php index a0c6e3f..cc55773 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php @@ -19,85 +19,46 @@ #[CoversClass(CustomerGroupApiController::class)] final class CustomerGroupApiControllerTest extends TestCase { - public function testGetCustomerGroupsReturnsJsonResponse(): void + public function testGetCustomerGroupsReturnsJsonResponseWithStatus200(): void { $sut = $this->getSut(); + $response = $sut->getCustomerGroups(); - $this->assertInstanceOf( - JsonResponse::class, - $sut->getCustomerGroups() - ); - } - - public function testGetCustomerGroupsReturnsStatus200(): void - { - $sut = $this->getSut(); - - $this->assertSame(200, $sut->getCustomerGroups()->getStatusCode()); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); } - public function testGetCustomerGroupsContainsGroupData(): void + public function testGetCustomerGroupsContainsGroupDataAndTotal(): void { - $groupId = uniqid('group_', true); - $title = uniqid('title_', true); - $count = mt_rand(1, 500); - - $serviceStub = $this->createStub(CustomerGroupServiceInterface::class); - $serviceStub->method('getCustomerGroupCounts') - ->willReturn([ + $serviceStub = $this->createConfiguredStub(CustomerGroupServiceInterface::class, [ + 'getCustomerGroupCounts' => [ new CustomerGroupCount( - groupId: $groupId, - title: $title, - count: $count, - ), - ]); - $serviceStub->method('getTotalCustomerCount') - ->willReturn($count); + groupId: $groupId = uniqid('group_'), + title: $title = uniqid('title_'), + count: $count = mt_rand(1, 500) + ) + ], + 'getTotalCustomerCount' => $total = mt_rand(100, 5000), + ]); $sut = $this->getSut(customerGroupService: $serviceStub); - - $data = $this->decodeResponse($sut->getCustomerGroups()); + $response = $sut->getCustomerGroups(); + $data = $this->decodeResponse($response); $this->assertCount(1, $data['customerGroups']); $this->assertSame($groupId, $data['customerGroups'][0]['groupId']); $this->assertSame($title, $data['customerGroups'][0]['title']); $this->assertSame($count, $data['customerGroups'][0]['count']); - } - - public function testGetCustomerGroupsContainsTotalCount(): void - { - $total = mt_rand(100, 5000); - - $serviceStub = $this->createStub(CustomerGroupServiceInterface::class); - $serviceStub->method('getCustomerGroupCounts') - ->willReturn([]); - $serviceStub->method('getTotalCustomerCount') - ->willReturn($total); - - $sut = $this->getSut(customerGroupService: $serviceStub); - - $data = $this->decodeResponse($sut->getCustomerGroups()); - $this->assertSame($total, $data['total']); } - public function testGetCustomerGroupsResponseStructure(): void - { - $sut = $this->getSut(); - - $data = $this->decodeResponse($sut->getCustomerGroups()); - - $this->assertArrayHasKey('customerGroups', $data); - $this->assertArrayHasKey('total', $data); - $this->assertCount(2, $data); - } - private function getSut( ?CustomerGroupServiceInterface $customerGroupService = null, ): CustomerGroupApiController { + $customerGroupService ??= $this->createStub(CustomerGroupServiceInterface::class); + return new CustomerGroupApiController( - customerGroupService: $customerGroupService - ?? $this->createStub(CustomerGroupServiceInterface::class), + customerGroupService: $customerGroupService, ); } diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php index 6ed7b94..f06a5a5 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php @@ -16,42 +16,16 @@ #[CoversClass(CustomerGroupCount::class)] final class CustomerGroupCountTest extends TestCase { - public function testGetGroupId(): void + public function testStoresGroupIdTitleAndCount(): void { - $groupId = uniqid('group_', true); + $groupId = uniqid(); + $title = uniqid(); + $count = mt_rand(0, 10000); - $sut = new CustomerGroupCount( - groupId: $groupId, - title: uniqid(), - count: mt_rand(0, 100), - ); + $sut = new CustomerGroupCount(groupId: $groupId, title: $title, count: $count); $this->assertSame($groupId, $sut->getGroupId()); - } - - public function testGetTitle(): void - { - $title = uniqid('title_', true); - - $sut = new CustomerGroupCount( - groupId: uniqid(), - title: $title, - count: mt_rand(0, 100), - ); - $this->assertSame($title, $sut->getTitle()); - } - - public function testGetCount(): void - { - $count = mt_rand(0, 10000); - - $sut = new CustomerGroupCount( - groupId: uniqid(), - title: uniqid(), - count: $count, - ); - $this->assertSame($count, $sut->getCount()); } } diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php index 1ff0235..86e0880 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php @@ -22,13 +22,13 @@ public function testGetCustomerGroupCountsDelegatesToDao(): void { $expectedCounts = [ new CustomerGroupCount( - groupId: uniqid('group_', true), - title: uniqid('title_', true), + groupId: uniqid('group_'), + title: uniqid('title_'), count: mt_rand(1, 500), ), new CustomerGroupCount( - groupId: uniqid('group_', true), - title: uniqid('title_', true), + groupId: uniqid('group_'), + title: uniqid('title_'), count: mt_rand(1, 500), ), ]; diff --git a/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php index 3d58c76..79ec435 100644 --- a/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php @@ -18,67 +18,38 @@ #[CoversClass(ProductInfoApiController::class)] final class ProductInfoApiControllerTest extends TestCase { - public function testGetProductInfoReturnsJsonResponse(): void + public function testGetProductInfoReturnsJsonResponseWithStatus200(): void { $sut = $this->getSut(); + $response = $sut->getProductInfo(); - $this->assertInstanceOf(JsonResponse::class, $sut->getProductInfo()); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); } - public function testGetProductInfoReturnsStatus200(): void - { - $sut = $this->getSut(); - - $this->assertSame(200, $sut->getProductInfo()->getStatusCode()); - } - - public function testGetProductInfoContainsProductCount(): void + public function testGetProductInfoContainsProductCountAndMessage(): void { $expectedCount = mt_rand(1, 10000); - - $serviceStub = $this->createStub(ProductInfoServiceInterface::class); - $serviceStub->method('getActiveProductCount') - ->willReturn($expectedCount); + $expectedMessage = uniqid('message_'); + $serviceStub = $this->createConfiguredStub(ProductInfoServiceInterface::class, [ + 'getActiveProductCount' => $expectedCount, + 'getGreetingMessage' => $expectedMessage, + ]); $sut = $this->getSut(productInfoService: $serviceStub); - $data = $this->decodeResponse($sut->getProductInfo()); $this->assertSame($expectedCount, $data['productCount']); - } - - public function testGetProductInfoContainsTranslatedMessage(): void - { - $expectedMessage = uniqid('message_', true); - - $serviceStub = $this->createStub(ProductInfoServiceInterface::class); - $serviceStub->method('getGreetingMessage') - ->willReturn($expectedMessage); - - $sut = $this->getSut(productInfoService: $serviceStub); - - $data = $this->decodeResponse($sut->getProductInfo()); - $this->assertSame($expectedMessage, $data['message']); } - public function testGetProductInfoResponseStructure(): void - { - $sut = $this->getSut(); - - $data = $this->decodeResponse($sut->getProductInfo()); - - $this->assertArrayHasKey('productCount', $data); - $this->assertArrayHasKey('message', $data); - $this->assertCount(2, $data); - } - private function getSut( ?ProductInfoServiceInterface $productInfoService = null, ): ProductInfoApiController { + $productInfoService ??= $this->createStub(ProductInfoServiceInterface::class); + return new ProductInfoApiController( - productInfoService: $productInfoService - ?? $this->createStub(ProductInfoServiceInterface::class), + productInfoService: $productInfoService, ); } diff --git a/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php b/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php index e4fa186..92f8bc5 100644 --- a/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php +++ b/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php @@ -21,7 +21,7 @@ final class ProductInfoServiceTest extends TestCase { public function testGetActiveProductCountDelegatesToDao(): void { - $expectedCount = mt_rand(1, 10000); + $expectedCount = mt_rand(0, 3); $daoStub = $this->createStub(ActiveProductCountDaoInterface::class); $daoStub->method('getActiveProductCount') @@ -32,27 +32,16 @@ public function testGetActiveProductCountDelegatesToDao(): void $this->assertSame($expectedCount, $sut->getActiveProductCount()); } - public function testGetActiveProductCountReturnsZeroWhenNoProducts(): void - { - $daoStub = $this->createStub(ActiveProductCountDaoInterface::class); - $daoStub->method('getActiveProductCount') - ->willReturn(0); - - $sut = $this->getSut(productCountDao: $daoStub); - - $this->assertSame(0, $sut->getActiveProductCount()); - } - public function testGetGreetingMessageTranslatesLanguageConstant(): void { - $expectedTranslation = uniqid('translation_', true); + $expectedTranslation = uniqid('translation_'); - $shopAdapterStub = $this->createStub(ShopAdapterInterface::class); - $shopAdapterStub->method('translateString') + $shopAdapterMock = $this->createStub(ShopAdapterInterface::class); + $shopAdapterMock->method('translateString') ->with(ModuleCore::API_HELLO_LANGUAGE_CONST) ->willReturn($expectedTranslation); - $sut = $this->getSut(shopAdapter: $shopAdapterStub); + $sut = $this->getSut(shopAdapter: $shopAdapterMock); $this->assertSame($expectedTranslation, $sut->getGreetingMessage()); } @@ -61,11 +50,12 @@ private function getSut( ?ActiveProductCountDaoInterface $productCountDao = null, ?ShopAdapterInterface $shopAdapter = null, ): ProductInfoService { + $productCountDao ??= $this->createStub(ActiveProductCountDaoInterface::class); + $shopAdapter ??= $this->createStub(ShopAdapterInterface::class); + return new ProductInfoService( - productCountDao: $productCountDao - ?? $this->createStub(ActiveProductCountDaoInterface::class), - shopAdapter: $shopAdapter - ?? $this->createStub(ShopAdapterInterface::class), + productCountDao: $productCountDao, + shopAdapter: $shopAdapter, ); } } diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php index 4c0936d..632cfaf 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php @@ -21,96 +21,51 @@ #[CoversClass(UserInfoApiController::class)] final class UserInfoApiControllerTest extends TestCase { - public function testGetUserInfoReturnsJsonResponse(): void + public function testGetUserInfoReturnsJsonResponseWithStatus200AndData(): void { - $sut = $this->getSut(); - $request = $this->createRequestWithUser(uniqid()); - - $this->assertInstanceOf( - JsonResponse::class, - $sut->getUserInfo($request) - ); - } - - public function testGetUserInfoReturnsFirstNameAndGreetingUrl(): void - { - $username = uniqid('user_', true); - $firstName = uniqid('name_', true); - $greetingUrl = uniqid('url_', true); + $username = uniqid('user_'); + $firstName = uniqid('name_'); + $greetingUrl = uniqid('url_'); $serviceStub = $this->createStub(UserInfoServiceInterface::class); $serviceStub->method('getUserInfo') ->with($username) - ->willReturn(new UserInfo( - firstName: $firstName, - greetingUrl: $greetingUrl, - )); + ->willReturn(new UserInfo(firstName: $firstName, greetingUrl: $greetingUrl)); $sut = $this->getSut(userInfoService: $serviceStub); - $request = $this->createRequestWithUser($username); - - $data = $this->decodeResponse($sut->getUserInfo($request)); + $response = $sut->getUserInfo($this->createRequestWithUser($username)); + $data = $this->decodeResponse($response); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); $this->assertSame($firstName, $data['firstName']); $this->assertSame($greetingUrl, $data['greetingUrl']); } - public function testGetUserInfoReturnsStatus200(): void - { - $username = uniqid('user_', true); - - $serviceStub = $this->createStub(UserInfoServiceInterface::class); - $serviceStub->method('getUserInfo') - ->willReturn(new UserInfo( - firstName: uniqid(), - greetingUrl: uniqid(), - )); - - $sut = $this->getSut(userInfoService: $serviceStub); - $request = $this->createRequestWithUser($username); - - $this->assertSame(200, $sut->getUserInfo($request)->getStatusCode()); - } - public function testGetUserInfoReturns404WhenUserNotFound(): void { + $username = uniqid('user_'); + $serviceStub = $this->createStub(UserInfoServiceInterface::class); $serviceStub->method('getUserInfo') + ->with($username) ->willReturn(null); $sut = $this->getSut(userInfoService: $serviceStub); - $request = $this->createRequestWithUser(uniqid()); + $request = $this->createRequestWithUser($username); $response = $sut->getUserInfo($request); $this->assertSame(404, $response->getStatusCode()); } - public function testGetUserInfoResponseStructure(): void - { - $serviceStub = $this->createStub(UserInfoServiceInterface::class); - $serviceStub->method('getUserInfo') - ->willReturn(new UserInfo( - firstName: uniqid(), - greetingUrl: uniqid(), - )); - - $sut = $this->getSut(userInfoService: $serviceStub); - $request = $this->createRequestWithUser(uniqid()); - - $data = $this->decodeResponse($sut->getUserInfo($request)); - - $this->assertArrayHasKey('firstName', $data); - $this->assertArrayHasKey('greetingUrl', $data); - $this->assertCount(2, $data); - } - private function getSut( ?UserInfoServiceInterface $userInfoService = null, ): UserInfoApiController { + $userInfoService ??= $this->createStub(UserInfoServiceInterface::class); + return new UserInfoApiController( - userInfoService: $userInfoService - ?? $this->createStub(UserInfoServiceInterface::class), + userInfoService: $userInfoService, ); } diff --git a/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php b/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php index 2d6848e..41ae00e 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php @@ -16,27 +16,17 @@ #[CoversClass(UserInfo::class)] final class UserInfoTest extends TestCase { - public function testGetFirstName(): void + public function testStoresFirstNameAndGreetingUrl(): void { - $firstName = uniqid('name_', true); + $firstName = uniqid('name_'); + $greetingUrl = uniqid('url_'); $sut = new UserInfo( firstName: $firstName, - greetingUrl: uniqid(), - ); - - $this->assertSame($firstName, $sut->getFirstName()); - } - - public function testGetGreetingUrl(): void - { - $greetingUrl = uniqid('url_', true); - - $sut = new UserInfo( - firstName: uniqid(), greetingUrl: $greetingUrl, ); + $this->assertSame($firstName, $sut->getFirstName()); $this->assertSame($greetingUrl, $sut->getGreetingUrl()); } } diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php index 9dda66d..3401fc8 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php @@ -18,51 +18,33 @@ #[CoversClass(UserInfoService::class)] final class UserInfoServiceTest extends TestCase { - public function testGetUserInfoReturnsFirstNameFromDao(): void + public function testGetUserInfoReturnsUserInfoWithFirstNameAndGreetingUrl(): void { - $username = uniqid('user_', true); - $expectedFirstName = uniqid('name_', true); + $username = uniqid('user_'); + $expectedFirstName = uniqid('name_'); - $daoStub = $this->createStub(SessionUserDaoInterface::class); - $daoStub->method('getFirstNameByUsername') - ->with($username) - ->willReturn($expectedFirstName); + $daoStub = $this->createConfiguredStub(SessionUserDaoInterface::class, [ + 'getFirstNameByUsername' => $expectedFirstName, + ]); $sut = $this->getSut(sessionUserDao: $daoStub); - $result = $sut->getUserInfo($username); + $this->assertInstanceOf(UserInfo::class, $result); $this->assertSame($expectedFirstName, $result->getFirstName()); - } - - public function testGetUserInfoReturnsGreetingUrl(): void - { - $username = uniqid('user_', true); - - $daoStub = $this->createStub(SessionUserDaoInterface::class); - $daoStub->method('getFirstNameByUsername') - ->willReturn(uniqid()); - - $sut = $this->getSut(sessionUserDao: $daoStub); - - $result = $sut->getUserInfo($username); - - $this->assertSame( - 'index.php?cl=oeem_greeting', - $result->getGreetingUrl() - ); + $this->assertSame('index.php?cl=oeem_greeting', $result->getGreetingUrl()); } public function testGetUserInfoReturnsNullWhenUserNotFound(): void { - $username = uniqid('unknown_', true); + $username = uniqid('unknown_'); - $daoStub = $this->createStub(SessionUserDaoInterface::class); - $daoStub->method('getFirstNameByUsername') + $daoMock = $this->createStub(SessionUserDaoInterface::class); + $daoMock->method('getFirstNameByUsername') ->with($username) ->willReturn(null); - $sut = $this->getSut(sessionUserDao: $daoStub); + $sut = $this->getSut(sessionUserDao: $daoMock); $this->assertNull($sut->getUserInfo($username)); } @@ -70,9 +52,10 @@ public function testGetUserInfoReturnsNullWhenUserNotFound(): void private function getSut( ?SessionUserDaoInterface $sessionUserDao = null, ): UserInfoService { + $sessionUserDao ??= $this->createStub(SessionUserDaoInterface::class); + return new UserInfoService( - sessionUserDao: $sessionUserDao - ?? $this->createStub(SessionUserDaoInterface::class), + sessionUserDao: $sessionUserDao, ); } } From 002d9f55978b6ab65e05e255669778595834b158 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 14:46:10 +0300 Subject: [PATCH 06/13] OXDEV-9653 Extract api entrypoint reusable parts to separate test case Signed-off-by: Anton Fedurtsya --- .../Controller/AdminInfoApiControllerTest.php | 24 +++----------- .../ApiEntrypoint/ApiEntrypointTestCase.php | 32 +++++++++++++++++++ .../CustomerGroupApiControllerTest.php | 9 ++---- .../ProductInfoApiControllerTest.php | 9 ++---- .../Controller/UserInfoApiControllerTest.php | 20 ++---------- 5 files changed, 42 insertions(+), 52 deletions(-) create mode 100644 tests/Unit/ApiEntrypoint/ApiEntrypointTestCase.php diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php index 03dd35d..b5e79f8 100644 --- a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php @@ -12,14 +12,12 @@ use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Controller\AdminInfoApiController; use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DataObject\AdminInfo; use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service\AdminInfoServiceInterface; +use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\User\InMemoryUser; #[CoversClass(AdminInfoApiController::class)] -final class AdminInfoApiControllerTest extends TestCase +final class AdminInfoApiControllerTest extends ApiEntrypointTestCase { public function testGetAdminInfoReturnsJsonResponseWithStatus200(): void { @@ -46,7 +44,7 @@ public function testGetAdminInfoReturnsEmailAndGreeting(): void $sut = $this->getSut(adminInfoService: $serviceStub); $response = $sut->getAdminInfo($this->createRequestWithUser($email)); - $data = $this->decodeJsonResponse($response); + $data = $this->decodeResponse($response); $this->assertSame($email, $data['email']); $this->assertSame($greeting, $data['greeting']); @@ -57,7 +55,7 @@ public function testGetAdminInfoResponseStructure(): void $sut = $this->getSut(); $response = $sut->getAdminInfo($this->createRequestWithUser(uniqid())); - $data = $this->decodeJsonResponse($response); + $data = $this->decodeResponse($response); $this->assertArrayHasKey('email', $data); $this->assertArrayHasKey('greeting', $data); @@ -80,18 +78,4 @@ private function getSut( adminInfoService: $adminInfoService, ); } - - private function createRequestWithUser(string $username): Request - { - $request = new Request(); - $user = new InMemoryUser($username, null, ['ROLE_USER', 'ROLE_ADMIN']); - $request->attributes->set('_user', $user); - - return $request; - } - - private function decodeJsonResponse(JsonResponse $response): array - { - return json_decode($response->getContent(), true); - } } diff --git a/tests/Unit/ApiEntrypoint/ApiEntrypointTestCase.php b/tests/Unit/ApiEntrypoint/ApiEntrypointTestCase.php new file mode 100644 index 0000000..10815d1 --- /dev/null +++ b/tests/Unit/ApiEntrypoint/ApiEntrypointTestCase.php @@ -0,0 +1,32 @@ +attributes->set('_user', $user); + + return $request; + } + + protected function decodeResponse(JsonResponse $response): array + { + return json_decode($response->getContent(), true); + } +} diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php index cc55773..958662b 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php @@ -12,12 +12,12 @@ use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Controller\CustomerGroupApiController; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupServiceInterface; +use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; #[CoversClass(CustomerGroupApiController::class)] -final class CustomerGroupApiControllerTest extends TestCase +final class CustomerGroupApiControllerTest extends ApiEntrypointTestCase { public function testGetCustomerGroupsReturnsJsonResponseWithStatus200(): void { @@ -61,9 +61,4 @@ private function getSut( customerGroupService: $customerGroupService, ); } - - private function decodeResponse(JsonResponse $response): array - { - return json_decode($response->getContent(), true); - } } diff --git a/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php index 79ec435..46c4c81 100644 --- a/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiControllerTest.php @@ -11,12 +11,12 @@ use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Controller\ProductInfoApiController; use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Service\ProductInfoServiceInterface; +use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; #[CoversClass(ProductInfoApiController::class)] -final class ProductInfoApiControllerTest extends TestCase +final class ProductInfoApiControllerTest extends ApiEntrypointTestCase { public function testGetProductInfoReturnsJsonResponseWithStatus200(): void { @@ -52,9 +52,4 @@ private function getSut( productInfoService: $productInfoService, ); } - - private function decodeResponse(JsonResponse $response): array - { - return json_decode($response->getContent(), true); - } } diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php index 632cfaf..533e09d 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php @@ -12,14 +12,12 @@ use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Controller\UserInfoApiController; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service\UserInfoServiceInterface; +use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\User\InMemoryUser; #[CoversClass(UserInfoApiController::class)] -final class UserInfoApiControllerTest extends TestCase +final class UserInfoApiControllerTest extends ApiEntrypointTestCase { public function testGetUserInfoReturnsJsonResponseWithStatus200AndData(): void { @@ -68,18 +66,4 @@ private function getSut( userInfoService: $userInfoService, ); } - - private function createRequestWithUser(string $username): Request - { - $request = new Request(); - $user = new InMemoryUser($username, null, ['ROLE_USER']); - $request->attributes->set('_user', $user); - - return $request; - } - - private function decodeResponse(JsonResponse $response): array - { - return json_decode($response->getContent(), true); - } } From 3033c0023b7160d9cc22d72b3ac42ca285b620b6 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 14:58:11 +0300 Subject: [PATCH 07/13] OXDEV-9653 Fix decoration bypass Signed-off-by: Anton Fedurtsya --- .../CustomerGroup/Service/CustomerGroupService.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php index 00801e8..12f4c7b 100644 --- a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php @@ -24,14 +24,13 @@ public function getCustomerGroupCounts(): array return $this->groupCountDao->getCustomerGroupCounts(); } - // todo-critical: direct usage of interface method is not possible public function getTotalCustomerCount(): int { - return array_sum( - array_map( - static fn($group) => $group->getCount(), - $this->getCustomerGroupCounts(), - ), - ); + $total = 0; + foreach ($this->groupCountDao->getCustomerGroupCounts() as $group) { + $total += $group->getCount(); + } + + return $total; } } From ef7a6922fe583df26f3ed567311b27172afb481e Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 14:59:46 +0300 Subject: [PATCH 08/13] OXDEV-9653 Replace nested array functions with foreach This requires much less cognetive power to understand the logic - better readability Signed-off-by: Anton Fedurtsya --- .../Controller/CustomerGroupApiController.php | 18 ++++++++++-------- .../Dao/CustomerGroupCountDao.php | 12 +++++++----- .../Service/CustomerGroupServiceTest.php | 5 +++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php b/src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php index e587642..f8cde8e 100644 --- a/src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php +++ b/src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php @@ -27,15 +27,17 @@ public function getCustomerGroups(): JsonResponse { $groups = $this->customerGroupService->getCustomerGroupCounts(); + $groupsData = []; + foreach ($groups as $group) { + $groupsData[] = [ + 'groupId' => $group->getGroupId(), + 'title' => $group->getTitle(), + 'count' => $group->getCount(), + ]; + } + return new JsonResponse([ - 'customerGroups' => array_map( - static fn($group) => [ - 'groupId' => $group->getGroupId(), - 'title' => $group->getTitle(), - 'count' => $group->getCount(), - ], - $groups, - ), + 'customerGroups' => $groupsData, 'total' => $this->customerGroupService->getTotalCustomerCount(), ]); } diff --git a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php b/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php index 1a10786..9ac8e98 100644 --- a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php +++ b/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php @@ -45,13 +45,15 @@ public function getCustomerGroupCounts(): array $result = $queryBuilder->execute(); $rows = $result->fetchAllAssociative(); - return array_values(array_map( - static fn(array $row) => new CustomerGroupCount( + $counts = []; + foreach ($rows as $row) { + $counts[] = new CustomerGroupCount( groupId: $row['groupId'], title: $row['title'], count: (int) $row['customerCount'], - ), - $rows, - )); + ); + } + + return $counts; } } diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php index 86e0880..389022b 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php @@ -92,9 +92,10 @@ public function testGetTotalCustomerCountReturnsZeroWhenNoGroups(): void private function getSut( ?CustomerGroupCountDaoInterface $groupCountDao = null, ): CustomerGroupService { + $groupCountDao ??= $this->createStub(CustomerGroupCountDaoInterface::class); + return new CustomerGroupService( - groupCountDao: $groupCountDao - ?? $this->createStub(CustomerGroupCountDaoInterface::class), + groupCountDao: $groupCountDao, ); } } From 1e204a1e6ef2c9d889b8570a390744bbf9f0c159 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 15:29:51 +0300 Subject: [PATCH 09/13] OXDEV-9653 Rename DAO to Repository Signed-off-by: Anton Fedurtsya --- .../CustomerGroupCountRepository.php} | 4 +-- ...CustomerGroupCountRepositoryInterface.php} | 4 +-- .../Service/CustomerGroupService.php | 8 ++--- src/ApiEntrypoint/CustomerGroup/services.yaml | 4 +-- .../ProductRepository.php} | 6 ++-- .../ProductRepositoryInterface.php} | 4 +-- .../Service/ProductInfoService.php | 6 ++-- src/ApiEntrypoint/ProductInfo/services.yaml | 4 +-- .../UserRepository.php} | 4 +-- .../UserRepositoryInterface.php} | 4 +-- .../UserInfo/Service/UserInfoService.php | 6 ++-- src/ApiEntrypoint/UserInfo/services.yaml | 4 +-- .../CustomerGroupCountRepositoryTest.php} | 18 +++++----- .../ProductRepositoryTest.php} | 12 +++---- .../UserRepositoryTest.php} | 16 ++++----- .../Service/CustomerGroupServiceTest.php | 34 +++++++++---------- .../Service/ProductInfoServiceTest.php | 16 ++++----- .../UserInfo/Service/UserInfoServiceTest.php | 18 +++++----- 18 files changed, 86 insertions(+), 86 deletions(-) rename src/ApiEntrypoint/CustomerGroup/{Dao/CustomerGroupCountDao.php => Infrastructure/CustomerGroupCountRepository.php} (93%) rename src/ApiEntrypoint/CustomerGroup/{Dao/CustomerGroupCountDaoInterface.php => Infrastructure/CustomerGroupCountRepositoryInterface.php} (85%) rename src/ApiEntrypoint/ProductInfo/{Dao/ActiveProductCountDao.php => Infrastructure/ProductRepository.php} (86%) rename src/ApiEntrypoint/ProductInfo/{Dao/ActiveProductCountDaoInterface.php => Infrastructure/ProductRepositoryInterface.php} (60%) rename src/ApiEntrypoint/UserInfo/{Dao/SessionUserDao.php => Infrastructure/UserRepository.php} (86%) rename src/ApiEntrypoint/UserInfo/{Dao/SessionUserDaoInterface.php => Infrastructure/UserRepositoryInterface.php} (64%) rename tests/Integration/ApiEntrypoint/CustomerGroup/{Dao/CustomerGroupCountDaoTest.php => Infrastructure/CustomerGroupCountRepositoryTest.php} (83%) rename tests/Integration/ApiEntrypoint/ProductInfo/{Dao/ActiveProductCountDaoTest.php => Infrastructure/ProductRepositoryTest.php} (90%) rename tests/Integration/ApiEntrypoint/UserInfo/{Dao/SessionUserDaoTest.php => Infrastructure/UserRepositoryTest.php} (76%) diff --git a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php b/src/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepository.php similarity index 93% rename from src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php rename to src/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepository.php index 9ac8e98..2ecc73f 100644 --- a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php +++ b/src/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepository.php @@ -7,13 +7,13 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure; use Doctrine\DBAL\Result; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; -readonly class CustomerGroupCountDao implements CustomerGroupCountDaoInterface +readonly class CustomerGroupCountRepository implements CustomerGroupCountRepositoryInterface { public function __construct( private QueryBuilderFactoryInterface $queryBuilderFactory, diff --git a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php b/src/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryInterface.php similarity index 85% rename from src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php rename to src/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryInterface.php index 75a9f2e..b35bbcd 100644 --- a/src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoInterface.php +++ b/src/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryInterface.php @@ -7,11 +7,11 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; -interface CustomerGroupCountDaoInterface +interface CustomerGroupCountRepositoryInterface { /** @return list */ public function getCustomerGroupCounts(): array; diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php index 12f4c7b..f489426 100644 --- a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php @@ -9,25 +9,25 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface; readonly class CustomerGroupService implements CustomerGroupServiceInterface { public function __construct( - private CustomerGroupCountDaoInterface $groupCountDao, + private CustomerGroupCountRepositoryInterface $groupCountRepository, ) { } /** @inheritDoc */ public function getCustomerGroupCounts(): array { - return $this->groupCountDao->getCustomerGroupCounts(); + return $this->groupCountRepository->getCustomerGroupCounts(); } public function getTotalCustomerCount(): int { $total = 0; - foreach ($this->groupCountDao->getCustomerGroupCounts() as $group) { + foreach ($this->groupCountRepository->getCustomerGroupCounts() as $group) { $total += $group->getCount(); } diff --git a/src/ApiEntrypoint/CustomerGroup/services.yaml b/src/ApiEntrypoint/CustomerGroup/services.yaml index 2520e80..368d462 100644 --- a/src/ApiEntrypoint/CustomerGroup/services.yaml +++ b/src/ApiEntrypoint/CustomerGroup/services.yaml @@ -3,8 +3,8 @@ services: public: false autowire: true - OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface: - class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDao + OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface: + class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepository OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupServiceInterface: class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupService diff --git a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php b/src/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepository.php similarity index 86% rename from src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php rename to src/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepository.php index dda9941..7f99f03 100644 --- a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDao.php +++ b/src/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepository.php @@ -7,14 +7,14 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure; use Doctrine\DBAL\Result; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface; use OxidEsales\EshopCommunity\Internal\Transition\Utility\ContextInterface; -readonly class ActiveProductCountDao implements ActiveProductCountDaoInterface +readonly class ProductRepository implements ProductRepositoryInterface { public function __construct( private QueryBuilderFactoryInterface $queryBuilderFactory, @@ -41,6 +41,6 @@ public function getActiveProductCount(): int /** @var Result $result */ $result = $queryBuilder->execute(); - return (int)$result->fetchOne(); + return (int) $result->fetchOne(); } } diff --git a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php b/src/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepositoryInterface.php similarity index 60% rename from src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php rename to src/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepositoryInterface.php index 2fb550c..06a1fd8 100644 --- a/src/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoInterface.php +++ b/src/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepositoryInterface.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure; -interface ActiveProductCountDaoInterface +interface ProductRepositoryInterface { public function getActiveProductCount(): int; } diff --git a/src/ApiEntrypoint/ProductInfo/Service/ProductInfoService.php b/src/ApiEntrypoint/ProductInfo/Service/ProductInfoService.php index 2356dec..1d0ebc5 100644 --- a/src/ApiEntrypoint/ProductInfo/Service/ProductInfoService.php +++ b/src/ApiEntrypoint/ProductInfo/Service/ProductInfoService.php @@ -11,19 +11,19 @@ use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface; use OxidEsales\ExamplesModule\Core\Module as ModuleCore; -use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure\ProductRepositoryInterface; readonly class ProductInfoService implements ProductInfoServiceInterface { public function __construct( - private ActiveProductCountDaoInterface $productCountDao, + private ProductRepositoryInterface $productRepository, private ShopAdapterInterface $shopAdapter, ) { } public function getActiveProductCount(): int { - return $this->productCountDao->getActiveProductCount(); + return $this->productRepository->getActiveProductCount(); } public function getGreetingMessage(): string diff --git a/src/ApiEntrypoint/ProductInfo/services.yaml b/src/ApiEntrypoint/ProductInfo/services.yaml index 669794b..b26df8b 100644 --- a/src/ApiEntrypoint/ProductInfo/services.yaml +++ b/src/ApiEntrypoint/ProductInfo/services.yaml @@ -3,8 +3,8 @@ services: public: false autowire: true - OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDaoInterface: - class: OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDao + OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure\ProductRepositoryInterface: + class: OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure\ProductRepository OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Service\ProductInfoServiceInterface: class: OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Service\ProductInfoService diff --git a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php b/src/ApiEntrypoint/UserInfo/Infrastructure/UserRepository.php similarity index 86% rename from src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php rename to src/ApiEntrypoint/UserInfo/Infrastructure/UserRepository.php index aa897fc..e4893a5 100644 --- a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDao.php +++ b/src/ApiEntrypoint/UserInfo/Infrastructure/UserRepository.php @@ -7,12 +7,12 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure; use Doctrine\DBAL\Result; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; -readonly class SessionUserDao implements SessionUserDaoInterface +readonly class UserRepository implements UserRepositoryInterface { public function __construct( private QueryBuilderFactoryInterface $queryBuilderFactory, diff --git a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php b/src/ApiEntrypoint/UserInfo/Infrastructure/UserRepositoryInterface.php similarity index 64% rename from src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php rename to src/ApiEntrypoint/UserInfo/Infrastructure/UserRepositoryInterface.php index 5a40ff2..fd1611e 100644 --- a/src/ApiEntrypoint/UserInfo/Dao/SessionUserDaoInterface.php +++ b/src/ApiEntrypoint/UserInfo/Infrastructure/UserRepositoryInterface.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure; -interface SessionUserDaoInterface +interface UserRepositoryInterface { public function getFirstNameByUsername(string $username): ?string; } diff --git a/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php b/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php index d02a3ef..a2c52d0 100644 --- a/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php +++ b/src/ApiEntrypoint/UserInfo/Service/UserInfoService.php @@ -9,7 +9,7 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao\SessionUserDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepositoryInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; readonly class UserInfoService implements UserInfoServiceInterface @@ -17,13 +17,13 @@ private const GREETING_CONTROLLER_URL = 'index.php?cl=oeem_greeting'; public function __construct( - private SessionUserDaoInterface $sessionUserDao, + private UserRepositoryInterface $userRepository, ) { } public function getUserInfo(string $username): ?UserInfo { - $firstName = $this->sessionUserDao->getFirstNameByUsername($username); + $firstName = $this->userRepository->getFirstNameByUsername($username); if ($firstName === null) { return null; diff --git a/src/ApiEntrypoint/UserInfo/services.yaml b/src/ApiEntrypoint/UserInfo/services.yaml index f834947..fb76057 100644 --- a/src/ApiEntrypoint/UserInfo/services.yaml +++ b/src/ApiEntrypoint/UserInfo/services.yaml @@ -3,8 +3,8 @@ services: public: false autowire: true - OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao\SessionUserDaoInterface: - class: OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao\SessionUserDao + OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepositoryInterface: + class: OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepository OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service\UserInfoServiceInterface: class: OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service\UserInfoService diff --git a/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php b/tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryTest.php similarity index 83% rename from tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php rename to tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryTest.php index b6e798b..bde9494 100644 --- a/tests/Integration/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDaoTest.php +++ b/tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryTest.php @@ -7,18 +7,18 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\CustomerGroup\Dao; +namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\CustomerGroup\Infrastructure; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDao; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepository; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; -#[CoversClass(CustomerGroupCountDao::class)] -final class CustomerGroupCountDaoTest extends IntegrationTestCase +#[CoversClass(CustomerGroupCountRepository::class)] +final class CustomerGroupCountRepositoryTest extends IntegrationTestCase { #[Before] public function cleanTables(): void @@ -30,7 +30,7 @@ public function cleanTables(): void #[Test] public function returnsEmptyArrayWhenNoActiveGroups(): void { - $sut = $this->get(CustomerGroupCountDaoInterface::class); + $sut = $this->get(CustomerGroupCountRepositoryInterface::class); $this->assertSame([], $sut->getCustomerGroupCounts()); } @@ -42,7 +42,7 @@ public function returnsGroupWithZeroCountWhenNoUsersAssigned(): void $groupTitle = uniqid('group_'); $this->createGroup($groupId, $groupTitle); - $sut = $this->get(CustomerGroupCountDaoInterface::class); + $sut = $this->get(CustomerGroupCountRepositoryInterface::class); $result = $sut->getCustomerGroupCounts(); $this->assertCount(1, $result); @@ -65,7 +65,7 @@ public function returnsCorrectCountPerGroup(): void ); } - $sut = $this->get(CustomerGroupCountDaoInterface::class); + $sut = $this->get(CustomerGroupCountRepositoryInterface::class); $result = $sut->getCustomerGroupCounts(); $this->assertSame($userCount, $result[0]->getCount()); @@ -80,7 +80,7 @@ public function inactiveGroupsAreExcluded(): void active: false, ); - $sut = $this->get(CustomerGroupCountDaoInterface::class); + $sut = $this->get(CustomerGroupCountRepositoryInterface::class); $this->assertSame([], $sut->getCustomerGroupCounts()); } diff --git a/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php b/tests/Integration/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepositoryTest.php similarity index 90% rename from tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php rename to tests/Integration/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepositoryTest.php index 0a41c9b..ce2dd5d 100644 --- a/tests/Integration/ApiEntrypoint/ProductInfo/Dao/ActiveProductCountDaoTest.php +++ b/tests/Integration/ApiEntrypoint/ProductInfo/Infrastructure/ProductRepositoryTest.php @@ -7,20 +7,20 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\ProductInfo\Dao; +namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\ProductInfo\Infrastructure; use OxidEsales\Eshop\Application\Model\Article; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface; use OxidEsales\EshopCommunity\Internal\Transition\Utility\ContextInterface; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; -use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDao; +use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure\ProductRepository; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; -#[CoversClass(ActiveProductCountDao::class)] -final class ActiveProductCountDaoTest extends IntegrationTestCase +#[CoversClass(ProductRepository::class)] +final class ProductRepositoryTest extends IntegrationTestCase { #[Before] public function cleanTables(): void @@ -72,9 +72,9 @@ public function countReflectsMultipleActiveProducts(): void $this->assertSame($count, $sut->getActiveProductCount()); } - private function getSut(): ActiveProductCountDao + private function getSut(): ProductRepository { - return new ActiveProductCountDao( + return new ProductRepository( queryBuilderFactory: $this->get(QueryBuilderFactoryInterface::class), shopAdapter: $this->get(ShopAdapterInterface::class), context: $this->get(ContextInterface::class), diff --git a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php b/tests/Integration/ApiEntrypoint/UserInfo/Infrastructure/UserRepositoryTest.php similarity index 76% rename from tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php rename to tests/Integration/ApiEntrypoint/UserInfo/Infrastructure/UserRepositoryTest.php index 0714b94..568d3c4 100644 --- a/tests/Integration/ApiEntrypoint/UserInfo/Dao/SessionUserDaoTest.php +++ b/tests/Integration/ApiEntrypoint/UserInfo/Infrastructure/UserRepositoryTest.php @@ -7,17 +7,17 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\UserInfo\Dao; +namespace OxidEsales\ExamplesModule\Tests\Integration\ApiEntrypoint\UserInfo\Infrastructure; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao\SessionUserDao; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao\SessionUserDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepository; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepositoryInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; -#[CoversClass(SessionUserDao::class)] -final class SessionUserDaoTest extends IntegrationTestCase +#[CoversClass(UserRepository::class)] +final class UserRepositoryTest extends IntegrationTestCase { #[Test] public function returnsFirstNameByUsername(): void @@ -27,7 +27,7 @@ public function returnsFirstNameByUsername(): void $this->createUser(username: $username, firstName: $firstName); - $sut = $this->get(SessionUserDaoInterface::class); + $sut = $this->get(UserRepositoryInterface::class); $this->assertSame($firstName, $sut->getFirstNameByUsername($username)); } @@ -35,7 +35,7 @@ public function returnsFirstNameByUsername(): void #[Test] public function returnsNullForNonExistentUser(): void { - $sut = $this->get(SessionUserDaoInterface::class); + $sut = $this->get(UserRepositoryInterface::class); $this->assertNull( $sut->getFirstNameByUsername(uniqid('unknown_') . '@example.com') @@ -49,7 +49,7 @@ public function returnsEmptyStringWhenFirstNameNotSet(): void $this->createUser(username: $username, firstName: ''); - $sut = $this->get(SessionUserDaoInterface::class); + $sut = $this->get(UserRepositoryInterface::class); $this->assertSame('', $sut->getFirstNameByUsername($username)); } diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php index 389022b..c14f1a9 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php @@ -9,7 +9,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\CustomerGroup\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupService; use PHPUnit\Framework\Attributes\CoversClass; @@ -18,7 +18,7 @@ #[CoversClass(CustomerGroupService::class)] final class CustomerGroupServiceTest extends TestCase { - public function testGetCustomerGroupCountsDelegatesToDao(): void + public function testGetCustomerGroupCountsDelegatesToRepository(): void { $expectedCounts = [ new CustomerGroupCount( @@ -33,22 +33,22 @@ public function testGetCustomerGroupCountsDelegatesToDao(): void ), ]; - $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); - $daoStub->method('getCustomerGroupCounts') + $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub->method('getCustomerGroupCounts') ->willReturn($expectedCounts); - $sut = $this->getSut(groupCountDao: $daoStub); + $sut = $this->getSut(groupCountRepository: $repositoryStub); $this->assertSame($expectedCounts, $sut->getCustomerGroupCounts()); } public function testGetCustomerGroupCountsReturnsEmptyArrayWhenNoGroups(): void { - $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); - $daoStub->method('getCustomerGroupCounts') + $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub->method('getCustomerGroupCounts') ->willReturn([]); - $sut = $this->getSut(groupCountDao: $daoStub); + $sut = $this->getSut(groupCountRepository: $repositoryStub); $this->assertSame([], $sut->getCustomerGroupCounts()); } @@ -58,8 +58,8 @@ public function testGetTotalCustomerCountSumsAllGroups(): void $count1 = mt_rand(1, 500); $count2 = mt_rand(1, 500); - $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); - $daoStub->method('getCustomerGroupCounts') + $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub->method('getCustomerGroupCounts') ->willReturn([ new CustomerGroupCount( groupId: uniqid(), @@ -73,29 +73,29 @@ public function testGetTotalCustomerCountSumsAllGroups(): void ), ]); - $sut = $this->getSut(groupCountDao: $daoStub); + $sut = $this->getSut(groupCountRepository: $repositoryStub); $this->assertSame($count1 + $count2, $sut->getTotalCustomerCount()); } public function testGetTotalCustomerCountReturnsZeroWhenNoGroups(): void { - $daoStub = $this->createStub(CustomerGroupCountDaoInterface::class); - $daoStub->method('getCustomerGroupCounts') + $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub->method('getCustomerGroupCounts') ->willReturn([]); - $sut = $this->getSut(groupCountDao: $daoStub); + $sut = $this->getSut(groupCountRepository: $repositoryStub); $this->assertSame(0, $sut->getTotalCustomerCount()); } private function getSut( - ?CustomerGroupCountDaoInterface $groupCountDao = null, + ?CustomerGroupCountRepositoryInterface $groupCountRepository = null, ): CustomerGroupService { - $groupCountDao ??= $this->createStub(CustomerGroupCountDaoInterface::class); + $groupCountRepository ??= $this->createStub(CustomerGroupCountRepositoryInterface::class); return new CustomerGroupService( - groupCountDao: $groupCountDao, + groupCountRepository: $groupCountRepository, ); } } diff --git a/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php b/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php index 92f8bc5..a1083a3 100644 --- a/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php +++ b/tests/Unit/ApiEntrypoint/ProductInfo/Service/ProductInfoServiceTest.php @@ -10,7 +10,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ProductInfo\Service; use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface; -use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Dao\ActiveProductCountDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Infrastructure\ProductRepositoryInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\ProductInfo\Service\ProductInfoService; use OxidEsales\ExamplesModule\Core\Module as ModuleCore; use PHPUnit\Framework\Attributes\CoversClass; @@ -19,15 +19,15 @@ #[CoversClass(ProductInfoService::class)] final class ProductInfoServiceTest extends TestCase { - public function testGetActiveProductCountDelegatesToDao(): void + public function testGetActiveProductCountDelegatesToRepository(): void { $expectedCount = mt_rand(0, 3); - $daoStub = $this->createStub(ActiveProductCountDaoInterface::class); - $daoStub->method('getActiveProductCount') + $repositoryStub = $this->createStub(ProductRepositoryInterface::class); + $repositoryStub->method('getActiveProductCount') ->willReturn($expectedCount); - $sut = $this->getSut(productCountDao: $daoStub); + $sut = $this->getSut(productRepository: $repositoryStub); $this->assertSame($expectedCount, $sut->getActiveProductCount()); } @@ -47,14 +47,14 @@ public function testGetGreetingMessageTranslatesLanguageConstant(): void } private function getSut( - ?ActiveProductCountDaoInterface $productCountDao = null, + ?ProductRepositoryInterface $productRepository = null, ?ShopAdapterInterface $shopAdapter = null, ): ProductInfoService { - $productCountDao ??= $this->createStub(ActiveProductCountDaoInterface::class); + $productRepository ??= $this->createStub(ProductRepositoryInterface::class); $shopAdapter ??= $this->createStub(ShopAdapterInterface::class); return new ProductInfoService( - productCountDao: $productCountDao, + productRepository: $productRepository, shopAdapter: $shopAdapter, ); } diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php index 3401fc8..db3c2d2 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php @@ -9,7 +9,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\UserInfo\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Dao\SessionUserDaoInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepositoryInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service\UserInfoService; use PHPUnit\Framework\Attributes\CoversClass; @@ -23,11 +23,11 @@ public function testGetUserInfoReturnsUserInfoWithFirstNameAndGreetingUrl(): voi $username = uniqid('user_'); $expectedFirstName = uniqid('name_'); - $daoStub = $this->createConfiguredStub(SessionUserDaoInterface::class, [ + $repositoryStub = $this->createConfiguredStub(UserRepositoryInterface::class, [ 'getFirstNameByUsername' => $expectedFirstName, ]); - $sut = $this->getSut(sessionUserDao: $daoStub); + $sut = $this->getSut(userRepository: $repositoryStub); $result = $sut->getUserInfo($username); $this->assertInstanceOf(UserInfo::class, $result); @@ -39,23 +39,23 @@ public function testGetUserInfoReturnsNullWhenUserNotFound(): void { $username = uniqid('unknown_'); - $daoMock = $this->createStub(SessionUserDaoInterface::class); - $daoMock->method('getFirstNameByUsername') + $repositoryStub = $this->createStub(UserRepositoryInterface::class); + $repositoryStub->method('getFirstNameByUsername') ->with($username) ->willReturn(null); - $sut = $this->getSut(sessionUserDao: $daoMock); + $sut = $this->getSut(userRepository: $repositoryStub); $this->assertNull($sut->getUserInfo($username)); } private function getSut( - ?SessionUserDaoInterface $sessionUserDao = null, + ?UserRepositoryInterface $userRepository = null, ): UserInfoService { - $sessionUserDao ??= $this->createStub(SessionUserDaoInterface::class); + $userRepository ??= $this->createStub(UserRepositoryInterface::class); return new UserInfoService( - sessionUserDao: $sessionUserDao, + userRepository: $userRepository, ); } } From cf18dc92ab33e8537283a7f40bcf5b714ac01add Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Wed, 29 Apr 2026 17:51:42 +0300 Subject: [PATCH 10/13] OXDEV-9653 Rename Data Object to DTO Signed-off-by: Anton Fedurtsya --- .../{DataObject => DTO}/AdminInfo.php | 4 +- .../AdminInfo/DTO/AdminInfoInterface.php | 17 +++++++ .../AdminInfo/Service/AdminInfoService.php | 5 ++- .../Service/AdminInfoServiceInterface.php | 4 +- .../CustomerGroupCount.php | 4 +- .../DTO/CustomerGroupCountInterface.php | 19 ++++++++ ...sitory.php => CustomerGroupRepository.php} | 4 +- ...p => CustomerGroupRepositoryInterface.php} | 6 +-- .../Service/CustomerGroupService.php | 4 +- .../Service/CustomerGroupServiceInterface.php | 4 +- src/ApiEntrypoint/CustomerGroup/services.yaml | 4 +- .../UserInfo/{DataObject => DTO}/UserInfo.php | 4 +- .../UserInfo/DTO/UserInfoInterface.php | 17 +++++++ .../UserInfo/Service/UserInfoService.php | 5 ++- .../Service/UserInfoServiceInterface.php | 4 +- ...st.php => CustomerGroupRepositoryTest.php} | 16 +++---- .../Controller/AdminInfoApiControllerTest.php | 16 +++---- .../{DataObject => DTO}/AdminInfoTest.php | 4 +- .../CustomerGroupApiControllerTest.php | 23 ++++++---- .../CustomerGroupCountTest.php | 4 +- .../Service/CustomerGroupServiceTest.php | 44 ++++++------------- .../Controller/UserInfoApiControllerTest.php | 9 +++- .../{DataObject => DTO}/UserInfoTest.php | 4 +- .../UserInfo/Service/UserInfoServiceTest.php | 4 +- 24 files changed, 138 insertions(+), 91 deletions(-) rename src/ApiEntrypoint/AdminInfo/{DataObject => DTO}/AdminInfo.php (77%) create mode 100644 src/ApiEntrypoint/AdminInfo/DTO/AdminInfoInterface.php rename src/ApiEntrypoint/CustomerGroup/{DataObject => DTO}/CustomerGroupCount.php (88%) create mode 100644 src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountInterface.php rename src/ApiEntrypoint/CustomerGroup/Infrastructure/{CustomerGroupCountRepository.php => CustomerGroupRepository.php} (89%) rename src/ApiEntrypoint/CustomerGroup/Infrastructure/{CustomerGroupCountRepositoryInterface.php => CustomerGroupRepositoryInterface.php} (58%) rename src/ApiEntrypoint/UserInfo/{DataObject => DTO}/UserInfo.php (78%) create mode 100644 src/ApiEntrypoint/UserInfo/DTO/UserInfoInterface.php rename tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/{CustomerGroupCountRepositoryTest.php => CustomerGroupRepositoryTest.php} (87%) rename tests/Unit/ApiEntrypoint/AdminInfo/{DataObject => DTO}/AdminInfoTest.php (87%) rename tests/Unit/ApiEntrypoint/CustomerGroup/{DataObject => DTO}/CustomerGroupCountTest.php (87%) rename tests/Unit/ApiEntrypoint/UserInfo/{DataObject => DTO}/UserInfoTest.php (88%) diff --git a/src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php b/src/ApiEntrypoint/AdminInfo/DTO/AdminInfo.php similarity index 77% rename from src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php rename to src/ApiEntrypoint/AdminInfo/DTO/AdminInfo.php index 7b7ef33..3b3d4fd 100644 --- a/src/ApiEntrypoint/AdminInfo/DataObject/AdminInfo.php +++ b/src/ApiEntrypoint/AdminInfo/DTO/AdminInfo.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DataObject; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO; -readonly class AdminInfo +readonly class AdminInfo implements AdminInfoInterface { public function __construct( private string $email, diff --git a/src/ApiEntrypoint/AdminInfo/DTO/AdminInfoInterface.php b/src/ApiEntrypoint/AdminInfo/DTO/AdminInfoInterface.php new file mode 100644 index 0000000..d430578 --- /dev/null +++ b/src/ApiEntrypoint/AdminInfo/DTO/AdminInfoInterface.php @@ -0,0 +1,17 @@ +shopAdapter->translateString( ModuleCore::ADMIN_HELLO_LANGUAGE_CONST diff --git a/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php b/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php index 3282bfd..35fa78f 100644 --- a/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php +++ b/src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php @@ -9,9 +9,9 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DataObject\AdminInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO\AdminInfoInterface; interface AdminInfoServiceInterface { - public function getAdminInfo(string $username): AdminInfo; + public function getAdminInfo(string $username): AdminInfoInterface; } diff --git a/src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php b/src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCount.php similarity index 88% rename from src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php rename to src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCount.php index 51e97b6..4ce22b1 100644 --- a/src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php +++ b/src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCount.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO; -readonly class CustomerGroupCount +readonly class CustomerGroupCount implements CustomerGroupCountInterface { public function __construct( private string $groupId, diff --git a/src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountInterface.php b/src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountInterface.php new file mode 100644 index 0000000..b820506 --- /dev/null +++ b/src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountInterface.php @@ -0,0 +1,19 @@ + */ + /** @return list */ public function getCustomerGroupCounts(): array; } diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php index f489426..91cebf8 100644 --- a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php @@ -9,12 +9,12 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupRepositoryInterface; readonly class CustomerGroupService implements CustomerGroupServiceInterface { public function __construct( - private CustomerGroupCountRepositoryInterface $groupCountRepository, + private CustomerGroupRepositoryInterface $groupCountRepository, ) { } diff --git a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php index 042e8eb..f0c3c83 100644 --- a/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php +++ b/src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceInterface.php @@ -9,11 +9,11 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO\CustomerGroupCountInterface; interface CustomerGroupServiceInterface { - /** @return list */ + /** @return list */ public function getCustomerGroupCounts(): array; public function getTotalCustomerCount(): int; diff --git a/src/ApiEntrypoint/CustomerGroup/services.yaml b/src/ApiEntrypoint/CustomerGroup/services.yaml index 368d462..bd0b116 100644 --- a/src/ApiEntrypoint/CustomerGroup/services.yaml +++ b/src/ApiEntrypoint/CustomerGroup/services.yaml @@ -3,8 +3,8 @@ services: public: false autowire: true - OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface: - class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepository + OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupRepositoryInterface: + class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupRepository OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupServiceInterface: class: OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupService diff --git a/src/ApiEntrypoint/UserInfo/DataObject/UserInfo.php b/src/ApiEntrypoint/UserInfo/DTO/UserInfo.php similarity index 78% rename from src/ApiEntrypoint/UserInfo/DataObject/UserInfo.php rename to src/ApiEntrypoint/UserInfo/DTO/UserInfo.php index 1bb2bad..41fc4a1 100644 --- a/src/ApiEntrypoint/UserInfo/DataObject/UserInfo.php +++ b/src/ApiEntrypoint/UserInfo/DTO/UserInfo.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject; +namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DTO; -readonly class UserInfo +readonly class UserInfo implements UserInfoInterface { public function __construct( private string $firstName, diff --git a/src/ApiEntrypoint/UserInfo/DTO/UserInfoInterface.php b/src/ApiEntrypoint/UserInfo/DTO/UserInfoInterface.php new file mode 100644 index 0000000..13b10b2 --- /dev/null +++ b/src/ApiEntrypoint/UserInfo/DTO/UserInfoInterface.php @@ -0,0 +1,17 @@ +userRepository->getFirstNameByUsername($username); diff --git a/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php b/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php index fecda7a..5eb8215 100644 --- a/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php +++ b/src/ApiEntrypoint/UserInfo/Service/UserInfoServiceInterface.php @@ -9,9 +9,9 @@ namespace OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DTO\UserInfoInterface; interface UserInfoServiceInterface { - public function getUserInfo(string $username): ?UserInfo; + public function getUserInfo(string $username): ?UserInfoInterface; } diff --git a/tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryTest.php b/tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupRepositoryTest.php similarity index 87% rename from tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryTest.php rename to tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupRepositoryTest.php index bde9494..af6d92a 100644 --- a/tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupCountRepositoryTest.php +++ b/tests/Integration/ApiEntrypoint/CustomerGroup/Infrastructure/CustomerGroupRepositoryTest.php @@ -11,14 +11,14 @@ use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepository; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupRepository; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupRepositoryInterface; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; -#[CoversClass(CustomerGroupCountRepository::class)] -final class CustomerGroupCountRepositoryTest extends IntegrationTestCase +#[CoversClass(CustomerGroupRepository::class)] +final class CustomerGroupRepositoryTest extends IntegrationTestCase { #[Before] public function cleanTables(): void @@ -30,7 +30,7 @@ public function cleanTables(): void #[Test] public function returnsEmptyArrayWhenNoActiveGroups(): void { - $sut = $this->get(CustomerGroupCountRepositoryInterface::class); + $sut = $this->get(CustomerGroupRepositoryInterface::class); $this->assertSame([], $sut->getCustomerGroupCounts()); } @@ -42,7 +42,7 @@ public function returnsGroupWithZeroCountWhenNoUsersAssigned(): void $groupTitle = uniqid('group_'); $this->createGroup($groupId, $groupTitle); - $sut = $this->get(CustomerGroupCountRepositoryInterface::class); + $sut = $this->get(CustomerGroupRepositoryInterface::class); $result = $sut->getCustomerGroupCounts(); $this->assertCount(1, $result); @@ -65,7 +65,7 @@ public function returnsCorrectCountPerGroup(): void ); } - $sut = $this->get(CustomerGroupCountRepositoryInterface::class); + $sut = $this->get(CustomerGroupRepositoryInterface::class); $result = $sut->getCustomerGroupCounts(); $this->assertSame($userCount, $result[0]->getCount()); @@ -80,7 +80,7 @@ public function inactiveGroupsAreExcluded(): void active: false, ); - $sut = $this->get(CustomerGroupCountRepositoryInterface::class); + $sut = $this->get(CustomerGroupRepositoryInterface::class); $this->assertSame([], $sut->getCustomerGroupCounts()); } diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php index b5e79f8..58cad45 100644 --- a/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiControllerTest.php @@ -10,7 +10,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\AdminInfo\Controller; use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Controller\AdminInfoApiController; -use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DataObject\AdminInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO\AdminInfoInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service\AdminInfoServiceInterface; use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -34,12 +34,13 @@ public function testGetAdminInfoReturnsEmailAndGreeting(): void $greeting = uniqid('greeting_'); $serviceStub = $this->createStub(AdminInfoServiceInterface::class); + $adminInfoStub = $this->createConfiguredStub(AdminInfoInterface::class, [ + 'getEmail' => $email, + 'getGreeting' => $greeting, + ]); $serviceStub->method('getAdminInfo') ->with($email) - ->willReturn(new AdminInfo( - email: $email, - greeting: $greeting, - )); + ->willReturn($adminInfoStub); $sut = $this->getSut(adminInfoService: $serviceStub); @@ -68,10 +69,7 @@ private function getSut( if ($adminInfoService === null) { $adminInfoService = $this->createStub(AdminInfoServiceInterface::class); $adminInfoService->method('getAdminInfo') - ->willReturn(new AdminInfo( - email: uniqid() . '@example.com', - greeting: uniqid(), - )); + ->willReturn($this->createStub(AdminInfoInterface::class)); } return new AdminInfoApiController( diff --git a/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php b/tests/Unit/ApiEntrypoint/AdminInfo/DTO/AdminInfoTest.php similarity index 87% rename from tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php rename to tests/Unit/ApiEntrypoint/AdminInfo/DTO/AdminInfoTest.php index eb71a9d..adfece5 100644 --- a/tests/Unit/ApiEntrypoint/AdminInfo/DataObject/AdminInfoTest.php +++ b/tests/Unit/ApiEntrypoint/AdminInfo/DTO/AdminInfoTest.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\AdminInfo\DataObject; +namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\AdminInfo\DTO; -use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DataObject\AdminInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO\AdminInfo; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php index 958662b..abbd6d7 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiControllerTest.php @@ -10,7 +10,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\CustomerGroup\Controller; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Controller\CustomerGroupApiController; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO\CustomerGroupCountInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupServiceInterface; use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -30,15 +30,20 @@ public function testGetCustomerGroupsReturnsJsonResponseWithStatus200(): void public function testGetCustomerGroupsContainsGroupDataAndTotal(): void { + $groupId = uniqid('group_'); + $title = uniqid('title_'); + $count = mt_rand(1, 500); + $total = mt_rand(100, 5000); + + $groupStub = $this->createConfiguredStub(CustomerGroupCountInterface::class, [ + 'getGroupId' => $groupId, + 'getTitle' => $title, + 'getCount' => $count, + ]); + $serviceStub = $this->createConfiguredStub(CustomerGroupServiceInterface::class, [ - 'getCustomerGroupCounts' => [ - new CustomerGroupCount( - groupId: $groupId = uniqid('group_'), - title: $title = uniqid('title_'), - count: $count = mt_rand(1, 500) - ) - ], - 'getTotalCustomerCount' => $total = mt_rand(100, 5000), + 'getCustomerGroupCounts' => [$groupStub], + 'getTotalCustomerCount' => $total, ]); $sut = $this->getSut(customerGroupService: $serviceStub); diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountTest.php similarity index 87% rename from tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php rename to tests/Unit/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountTest.php index f06a5a5..6f788c0 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCountTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCountTest.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\CustomerGroup\DataObject; +namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\CustomerGroup\DTO; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO\CustomerGroupCount; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php index c14f1a9..f5ccea4 100644 --- a/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php +++ b/tests/Unit/ApiEntrypoint/CustomerGroup/Service/CustomerGroupServiceTest.php @@ -9,8 +9,8 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\CustomerGroup\Service; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupCountRepositoryInterface; -use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure\CustomerGroupRepositoryInterface; +use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO\CustomerGroupCountInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupService; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -21,19 +21,11 @@ final class CustomerGroupServiceTest extends TestCase public function testGetCustomerGroupCountsDelegatesToRepository(): void { $expectedCounts = [ - new CustomerGroupCount( - groupId: uniqid('group_'), - title: uniqid('title_'), - count: mt_rand(1, 500), - ), - new CustomerGroupCount( - groupId: uniqid('group_'), - title: uniqid('title_'), - count: mt_rand(1, 500), - ), + $this->createStub(CustomerGroupCountInterface::class), + $this->createStub(CustomerGroupCountInterface::class), ]; - $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub = $this->createStub(CustomerGroupRepositoryInterface::class); $repositoryStub->method('getCustomerGroupCounts') ->willReturn($expectedCounts); @@ -44,7 +36,7 @@ public function testGetCustomerGroupCountsDelegatesToRepository(): void public function testGetCustomerGroupCountsReturnsEmptyArrayWhenNoGroups(): void { - $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub = $this->createStub(CustomerGroupRepositoryInterface::class); $repositoryStub->method('getCustomerGroupCounts') ->willReturn([]); @@ -58,20 +50,12 @@ public function testGetTotalCustomerCountSumsAllGroups(): void $count1 = mt_rand(1, 500); $count2 = mt_rand(1, 500); - $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $group1 = $this->createConfiguredStub(CustomerGroupCountInterface::class, ['getCount' => $count1]); + $group2 = $this->createConfiguredStub(CustomerGroupCountInterface::class, ['getCount' => $count2]); + + $repositoryStub = $this->createStub(CustomerGroupRepositoryInterface::class); $repositoryStub->method('getCustomerGroupCounts') - ->willReturn([ - new CustomerGroupCount( - groupId: uniqid(), - title: uniqid(), - count: $count1, - ), - new CustomerGroupCount( - groupId: uniqid(), - title: uniqid(), - count: $count2, - ), - ]); + ->willReturn([$group1, $group2]); $sut = $this->getSut(groupCountRepository: $repositoryStub); @@ -80,7 +64,7 @@ public function testGetTotalCustomerCountSumsAllGroups(): void public function testGetTotalCustomerCountReturnsZeroWhenNoGroups(): void { - $repositoryStub = $this->createStub(CustomerGroupCountRepositoryInterface::class); + $repositoryStub = $this->createStub(CustomerGroupRepositoryInterface::class); $repositoryStub->method('getCustomerGroupCounts') ->willReturn([]); @@ -90,9 +74,9 @@ public function testGetTotalCustomerCountReturnsZeroWhenNoGroups(): void } private function getSut( - ?CustomerGroupCountRepositoryInterface $groupCountRepository = null, + ?CustomerGroupRepositoryInterface $groupCountRepository = null, ): CustomerGroupService { - $groupCountRepository ??= $this->createStub(CustomerGroupCountRepositoryInterface::class); + $groupCountRepository ??= $this->createStub(CustomerGroupRepositoryInterface::class); return new CustomerGroupService( groupCountRepository: $groupCountRepository, diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php index 533e09d..fb822cf 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/Controller/UserInfoApiControllerTest.php @@ -10,7 +10,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\UserInfo\Controller; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Controller\UserInfoApiController; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DTO\UserInfoInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service\UserInfoServiceInterface; use OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\ApiEntrypointTestCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -25,10 +25,15 @@ public function testGetUserInfoReturnsJsonResponseWithStatus200AndData(): void $firstName = uniqid('name_'); $greetingUrl = uniqid('url_'); + $userInfoStub = $this->createConfiguredStub(UserInfoInterface::class, [ + 'getFirstName' => $firstName, + 'getGreetingUrl' => $greetingUrl, + ]); + $serviceStub = $this->createStub(UserInfoServiceInterface::class); $serviceStub->method('getUserInfo') ->with($username) - ->willReturn(new UserInfo(firstName: $firstName, greetingUrl: $greetingUrl)); + ->willReturn($userInfoStub); $sut = $this->getSut(userInfoService: $serviceStub); $response = $sut->getUserInfo($this->createRequestWithUser($username)); diff --git a/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php b/tests/Unit/ApiEntrypoint/UserInfo/DTO/UserInfoTest.php similarity index 88% rename from tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php rename to tests/Unit/ApiEntrypoint/UserInfo/DTO/UserInfoTest.php index 41ae00e..d1d7e40 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/DataObject/UserInfoTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/DTO/UserInfoTest.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\UserInfo\DataObject; +namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\UserInfo\DTO; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DTO\UserInfo; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php index db3c2d2..732029c 100644 --- a/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php +++ b/tests/Unit/ApiEntrypoint/UserInfo/Service/UserInfoServiceTest.php @@ -10,7 +10,7 @@ namespace OxidEsales\ExamplesModule\Tests\Unit\ApiEntrypoint\UserInfo\Service; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Infrastructure\UserRepositoryInterface; -use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DataObject\UserInfo; +use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\DTO\UserInfoInterface; use OxidEsales\ExamplesModule\ApiEntrypoint\UserInfo\Service\UserInfoService; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -30,7 +30,7 @@ public function testGetUserInfoReturnsUserInfoWithFirstNameAndGreetingUrl(): voi $sut = $this->getSut(userRepository: $repositoryStub); $result = $sut->getUserInfo($username); - $this->assertInstanceOf(UserInfo::class, $result); + $this->assertInstanceOf(UserInfoInterface::class, $result); $this->assertSame($expectedFirstName, $result->getFirstName()); $this->assertSame('index.php?cl=oeem_greeting', $result->getGreetingUrl()); } From 3ca3f55b3011888f7d4db7d074657704756e13d4 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Thu, 30 Apr 2026 14:07:51 +0300 Subject: [PATCH 11/13] OXDEV-9653 Improve API entrypoint codeception tests * Move api entrypoint tests to separate directory * Improved readability of JS calls * Removed some tests that covers the same stuff as we have already covered by integrations Signed-off-by: Anton Fedurtsya --- .../{ => ApiEntrypoint}/AdminInfoApiCest.php | 14 +- .../ApiEntrypoint/CustomerGroupApiCest.php | 120 +++++++++++++++ .../ProductInfoApiCest.php | 30 ++-- .../UserInfoGreetingButtonCest.php | 2 +- .../Acceptance/CustomerGroupApiCest.php | 140 ------------------ 5 files changed, 144 insertions(+), 162 deletions(-) rename tests/Codeception/Acceptance/{ => ApiEntrypoint}/AdminInfoApiCest.php (83%) create mode 100644 tests/Codeception/Acceptance/ApiEntrypoint/CustomerGroupApiCest.php rename tests/Codeception/Acceptance/{ => ApiEntrypoint}/ProductInfoApiCest.php (75%) rename tests/Codeception/Acceptance/{ => ApiEntrypoint}/UserInfoGreetingButtonCest.php (99%) delete mode 100644 tests/Codeception/Acceptance/CustomerGroupApiCest.php diff --git a/tests/Codeception/Acceptance/AdminInfoApiCest.php b/tests/Codeception/Acceptance/ApiEntrypoint/AdminInfoApiCest.php similarity index 83% rename from tests/Codeception/Acceptance/AdminInfoApiCest.php rename to tests/Codeception/Acceptance/ApiEntrypoint/AdminInfoApiCest.php index 15c9999..99d115d 100644 --- a/tests/Codeception/Acceptance/AdminInfoApiCest.php +++ b/tests/Codeception/Acceptance/ApiEntrypoint/AdminInfoApiCest.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace OxidEsales\ExamplesModule\Tests\Codeception\Acceptance; +namespace OxidEsales\ExamplesModule\Tests\Codeception\Acceptance\ApiEntrypoint; use Codeception\Attribute\Group; use Codeception\Util\Fixtures; @@ -59,12 +59,12 @@ public function testAdminInfoApiRejectsUnauthenticated( $I->openShop(); $I->waitForPageLoad(); - $response = $I->executeAsyncJS( - "var callback = arguments[arguments.length - 1];" - . "fetch('/api/admin-info')" - . ".then(function(r) { callback({status: r.status}); })" - . ".catch(function(e) { callback({status: 0}); });" - ); + $response = $I->executeAsyncJS(<<assertSame(401, $response['status']); } diff --git a/tests/Codeception/Acceptance/ApiEntrypoint/CustomerGroupApiCest.php b/tests/Codeception/Acceptance/ApiEntrypoint/CustomerGroupApiCest.php new file mode 100644 index 0000000..12f1a96 --- /dev/null +++ b/tests/Codeception/Acceptance/ApiEntrypoint/CustomerGroupApiCest.php @@ -0,0 +1,120 @@ +wantToTest('customer-groups rejects unauthenticated requests'); + + $I->openShop(); + $I->waitForPageLoad(); + + $response = $this->fetchApi($I, '/api/customer-groups'); + + $I->assertSame(401, $response['status']); + } + + public function testCustomerGroupsReturnsDataWithAdminToken( + AcceptanceTester $I + ): void { + $I->wantToTest('customer-groups returns data for admin'); + + $I->openShop(); + $I->waitForPageLoad(); + + $admin = Fixtures::get('adminUser'); + $token = $this->loginViaApi($I, $admin['email'], $admin['password']); + + $response = $this->fetchApiWithToken( + $I, + '/api/customer-groups', + $token + ); + + $I->assertSame(200, $response['status']); + $I->assertArrayHasKey('customerGroups', $response['body']); + $I->assertArrayHasKey('total', $response['body']); + } + + + private function loginViaApi( + AcceptanceTester $I, + string $username, + string $password + ): string { + $result = $I->executeAsyncJS(<<executeAsyncJS(<<executeAsyncJS(<<executeAsyncJS( - "var callback = arguments[arguments.length - 1];" - . "fetch('" . $path . "')" - . ".then(function(r) {" - . " var status = r.status;" - . " return r.json().then(function(b) {" - . " callback({status: status, body: b});" - . " });" - . "})" - . ".catch(function(e) {" - . " callback({status: 0, body: {error: e.message}});" - . "});" - ); + return $I->executeAsyncJS(<<wantToTest('customer-groups rejects unauthenticated requests'); - - $I->openShop(); - $I->waitForPageLoad(); - - $response = $this->fetchApi($I, '/api/customer-groups'); - - $I->assertSame(401, $response['status']); - } - - public function testCustomerGroupsReturnsDataWithAdminToken( - AcceptanceTester $I - ): void { - $I->wantToTest('customer-groups returns data for admin'); - - $I->openShop(); - $I->waitForPageLoad(); - - $admin = Fixtures::get('adminUser'); - $token = $this->loginViaApi($I, $admin['email'], $admin['password']); - - $response = $this->fetchApiWithToken( - $I, - '/api/customer-groups', - $token - ); - - $I->assertSame(200, $response['status']); - $I->assertArrayHasKey('customerGroups', $response['body']); - $I->assertArrayHasKey('total', $response['body']); - } - - public function testCustomerGroupsContainsGroupStructure( - AcceptanceTester $I - ): void { - $I->wantToTest('customer group entries have correct structure'); - - $I->openShop(); - $I->waitForPageLoad(); - - $admin = Fixtures::get('adminUser'); - $token = $this->loginViaApi($I, $admin['email'], $admin['password']); - - $response = $this->fetchApiWithToken( - $I, - '/api/customer-groups', - $token - ); - - $I->assertNotEmpty($response['body']['customerGroups']); - $group = $response['body']['customerGroups'][0]; - $I->assertArrayHasKey('groupId', $group); - $I->assertArrayHasKey('title', $group); - $I->assertArrayHasKey('count', $group); - } - - private function loginViaApi( - AcceptanceTester $I, - string $username, - string $password - ): string { - $result = $I->executeAsyncJS( - "var callback = arguments[arguments.length - 1];" - . "fetch('/api/login', {" - . " method: 'POST'," - . " headers: {'Content-Type': 'application/json'}," - . " body: JSON.stringify({" - . " username: '" . $username . "'," - . " password: '" . $password . "'" - . " })" - . "})" - . ".then(function(r) { return r.json(); })" - . ".then(function(b) { callback(b); })" - . ".catch(function(e) { callback({error: e.message}); });" - ); - - return $result['token']; - } - - private function fetchApi(AcceptanceTester $I, string $path): array - { - return $I->executeAsyncJS( - "var callback = arguments[arguments.length - 1];" - . "fetch('" . $path . "')" - . ".then(function(r) {" - . " var status = r.status;" - . " return r.json().then(function(b) {" - . " callback({status: status, body: b});" - . " });" - . "})" - . ".catch(function(e) {" - . " callback({status: 0, body: {error: e.message}});" - . "});" - ); - } - - private function fetchApiWithToken( - AcceptanceTester $I, - string $path, - string $token - ): array { - return $I->executeAsyncJS( - "var callback = arguments[arguments.length - 1];" - . "fetch('" . $path . "', {" - . " headers: {'Authorization': 'Bearer " . $token . "'}" - . "})" - . ".then(function(r) {" - . " var status = r.status;" - . " return r.json().then(function(b) {" - . " callback({status: status, body: b});" - . " });" - . "})" - . ".catch(function(e) {" - . " callback({status: 0, body: {error: e.message}});" - . "});" - ); - } -} From 6e62e59d02ee604db109095c63012965b62ae1c2 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Thu, 30 Apr 2026 15:23:04 +0300 Subject: [PATCH 12/13] OXDEV-9653 Update readme Signed-off-by: Anton Fedurtsya --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2ce23fe..f0b9c6d 100644 --- a/README.md +++ b/README.md @@ -150,25 +150,27 @@ The repository contains examples of following cases and more: * **Public endpoint** — [ProductInfo](src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php): `GET /api/product-info` * No authentication required * Returns JSON with active product count of current shop and translated greeting message - * Demonstrates `#[Route]` attribute, service injection, DAO pattern, and translation via `ShopAdapterInterface` + * Demonstrates: + * `#[Route]` attribute + * service injection + * shop/language-aware database view via `ShopAdapterInterface::generateDatabaseViewName()` + * translation via `ShopAdapterInterface` * **JWT-protected endpoint** — [CustomerGroup](src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php): `GET /api/customer-groups` * Requires `#[IsGranted('ROLE_ADMIN')]` — admin JWT token via `Authorization: Bearer` - * Returns customer counts per user group (sensitive business data) - * Demonstrates readonly DTO ([CustomerGroupCount](src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php)), LEFT JOIN in DAO * Requires `oxid-esales/jwt-authentication-component` — obtain a token via `POST /api/login` (see [JWT component README](https://github.com/OXID-eSales/jwt-authentication-component#login) for details) + * Returns customer counts per user group (sensitive business data) * **Frontend session endpoint** — [UserInfo](src/ApiEntrypoint/UserInfo/Controller/UserInfoApiController.php): `GET /api/user-info` * Requires `#[SessionUser]` — active frontend session (`sid` cookie) + * Requires `oxid-esales/session-authentication-component` * Returns logged-in user's first name and greeting controller URL * Demonstrates storefront AJAX use case: [header button](views/twig/extensions/themes/default/layout/header.html.twig) fetches endpoint and shows personalized greeting link - * Requires `oxid-esales/session-authentication-component` * **Admin session endpoint** — [AdminInfo](src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php): `GET /api/admin-info` * Requires `#[AdminSessionUser(roles: ['ROLE_ADMIN'])]` — active admin session (`admin_sid` cookie) + * Requires `oxid-esales/session-authentication-component` * Returns translated greeting with admin email (e.g. "Hello, Admin admin@example.com") * Demonstrates admin AJAX use case: [admin header greeting](views/twig/extensions/themes/admin_twig/include/header_links.html.twig) - * Requires `oxid-esales/session-authentication-component` - * Each example follows the same layered structure: Controller → Service (interface) → DAO (interface) → DataObject - * [Service wiring](src/ApiEntrypoint/ProductInfo/services.yaml) — public controller, private service and DAO - + * Each example follows a layered structure with interfaces at every boundary (Controller → Service → Repository → DTO where applicable) + **HINTS**: * Only extend the shop core if there is no other way like listen and handle shop events, decorate/replace some DI service. From 3432a2a4a03f4821251a62eaf68d50250685ab94 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Thu, 30 Apr 2026 15:38:59 +0300 Subject: [PATCH 13/13] OXDEV-9653 Move critical dependencies to require section Signed-off-by: Anton Fedurtsya --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a060760..278c0d8 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,9 @@ "require": { "php": "^8.3", "symfony/filesystem": "^6.4", - "ddoe/wysiwyg-editor-module": "dev-b-7.5.x" + "ddoe/wysiwyg-editor-module": "dev-b-7.5.x", + "oxid-esales/jwt-authentication-component": "dev-b-7.5.x", + "oxid-esales/session-authentication-component": "dev-b-7.5.x" }, "minimum-stability": "dev", "prefer-stable": true, @@ -31,9 +33,7 @@ "codeception/module-webdriver": "^4.0", "oxid-esales/codeception-modules": "dev-b-7.5.x", "oxid-esales/codeception-page-objects": "dev-b-7.5.x", - "oxid-esales/developer-tools": "dev-b-7.5.x", - "oxid-esales/jwt-authentication-component": "dev-b-7.5.x", - "oxid-esales/session-authentication-component": "dev-b-7.5.x" + "oxid-esales/developer-tools": "dev-b-7.5.x" }, "conflict": { "oxid-esales/oxideshop-ce": "<7.5"