diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d8145b9 --- /dev/null +++ b/composer.json @@ -0,0 +1,10 @@ +{ + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + } +} diff --git a/docs/TESTING.md b/docs/TESTING.md index 2b56703..ff0f547 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,106 +1,54 @@ -# Testowanie shopPRO +# Testowanie cmsPRO ## Szybki start ```bash -# Pelny suite (PowerShell — rekomendowane) -./test.ps1 +# Instalacja PHPUnit (jednorazowo) +composer install + +# Uruchomienie testów +./vendor/bin/phpunit # Konkretny plik -./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php +./vendor/bin/phpunit tests/Unit/Domain/Settings/SettingsRepositoryTest.php # Konkretny test -./test.ps1 --filter testGetQuantityReturnsCorrectValue - -# Alternatywne -composer test # standard -./test.bat # testdox (czytelna lista) -./test-simple.bat # kropki -./test-debug.bat # debug -./test.sh # Git Bash +./vendor/bin/phpunit --filter testAllSettingsReturnsMappedArray ``` ## Aktualny stan ```text -OK (805 tests, 2253 assertions) +Testy jednostkowe dla Domain\ (Faza 2 DDD) ``` -Zweryfikowano: 2026-02-24 (ver. 0.318) - ## Konfiguracja -- **PHPUnit 9.6** via `phpunit.phar` +- **PHPUnit 10** via `composer` - **Bootstrap:** `tests/bootstrap.php` - **Config:** `phpunit.xml` -## Struktura testow +## Struktura testów ``` tests/ -|-- bootstrap.php -|-- stubs/ -| |-- CacheHandler.php (inline w bootstrap) -| |-- Helpers.php (Shared\Helpers\Helpers stub) -| `-- ShopProduct.php (shop\Product stub) -|-- Unit/ -| |-- Domain/ -| | |-- Article/ArticleRepositoryTest.php -| | |-- Attribute/AttributeRepositoryTest.php -| | |-- Banner/BannerRepositoryTest.php -| | |-- Basket/BasketCalculatorTest.php -| | |-- Cache/CacheRepositoryTest.php -| | |-- Category/CategoryRepositoryTest.php -| | |-- Coupon/CouponRepositoryTest.php -| | |-- CronJob/CronJobTypeTest.php -| | |-- CronJob/CronJobRepositoryTest.php -| | |-- CronJob/CronJobProcessorTest.php -| | |-- Dictionaries/DictionariesRepositoryTest.php -| | |-- Integrations/IntegrationsRepositoryTest.php -| | |-- Languages/LanguagesRepositoryTest.php -| | |-- Layouts/LayoutsRepositoryTest.php -| | |-- Newsletter/NewsletterRepositoryTest.php -| | |-- Pages/PagesRepositoryTest.php -| | |-- PaymentMethod/PaymentMethodRepositoryTest.php -| | |-- Producer/ProducerRepositoryTest.php -| | |-- Product/ProductRepositoryTest.php -| | |-- ProductSet/ProductSetRepositoryTest.php -| | |-- Promotion/PromotionRepositoryTest.php -| | |-- Settings/SettingsRepositoryTest.php -| | |-- ShopStatus/ShopStatusRepositoryTest.php -| | |-- Transport/TransportRepositoryTest.php -| | |-- Update/UpdateRepositoryTest.php -| | `-- User/UserRepositoryTest.php -| `-- admin/ -| `-- Controllers/ -| |-- ArticlesControllerTest.php -| |-- DictionariesControllerTest.php -| |-- IntegrationsControllerTest.php -| |-- ProductArchiveControllerTest.php -| |-- SettingsControllerTest.php -| |-- ShopAttributeControllerTest.php -| |-- ShopCategoryControllerTest.php -| |-- ShopCouponControllerTest.php -| |-- ShopPaymentMethodControllerTest.php -| |-- ShopProducerControllerTest.php -| |-- ShopProductControllerTest.php -| |-- ShopProductSetsControllerTest.php -| |-- ShopPromotionControllerTest.php -| |-- ShopStatusesControllerTest.php -| |-- ShopTransportControllerTest.php -| `-- UsersControllerTest.php -| `-- api/ -| |-- ApiRouterTest.php -| `-- Controllers/ -| |-- OrdersApiControllerTest.php -| |-- ProductsApiControllerTest.php -| `-- DictionariesApiControllerTest.php -`-- Integration/ (puste — zarezerwowane) +├── bootstrap.php ← autoloader + stuby (CacheHandler, S) +└── Unit/ + └── Domain/ + ├── Languages/LanguagesRepositoryTest.php + ├── Settings/SettingsRepositoryTest.php + └── User/UserRepositoryTest.php ``` -## Dodawanie nowych testow +## Stuby (bootstrap.php) -1. Plik w `tests/Unit/Domain//Test.php`, `tests/Unit/admin/Controllers/Test.php` lub `tests/Unit/api/Controllers/Test.php`. +- `\Shared\Cache\CacheHandler` — in-memory stub z `fetch()`/`store()`/`delete()`/`reset()` +- `\S` — stub z `delete_cache()`, `htacces()`, `get_domain()`, `send_email()` +- `medoo` — mockowany przez PHPUnit (`$this->createMock(\medoo::class)`) + +## Dodawanie nowych testów + +1. Plik w `tests/Unit/Domain//Test.php`. 2. Rozszerz `PHPUnit\Framework\TestCase`. 3. Nazwy metod zaczynaj od `test`. 4. Wzorzec AAA: Arrange, Act, Assert. @@ -108,19 +56,11 @@ tests/ ## Mockowanie Medoo ```php -$mockDb = $this->createMock(\medoo::class); -$mockDb->method('get')->willReturn(42); +$db = $this->createMock(\medoo::class); +$db->method('get')->willReturn(['id' => 1]); -$repo = new ProductRepository($mockDb); -$value = $repo->getQuantity(123); +$repo = new SettingsRepository($db); +$value = $repo->visitCounter(); -$this->assertEquals(42, $value); +$this->assertSame('1', $value); ``` - -## Bootstrap — stuby - -`tests/bootstrap.php` rejestruje autoloader i definiuje stuby: -- `Redis`, `RedisConnection` — klasy Redis (aby nie wymagac rozszerzenia) -- `Shared\Cache\CacheHandler` — inline stub z `get()`/`set()`/`exists()`/`delete()`/`deletePattern()` -- `Shared\Helpers\Helpers` — z `tests/stubs/Helpers.php` -- `shop\Product` — z `tests/stubs/ShopProduct.php` diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..397b728 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,20 @@ + + + + + + tests/Unit + + + + + + autoload/Domain + autoload/Shared + + + + diff --git a/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php b/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php new file mode 100644 index 0000000..4a3dd68 --- /dev/null +++ b/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php @@ -0,0 +1,123 @@ +createMock(\medoo::class); + } + + protected function setUp(): void + { + \Shared\Cache\CacheHandler::reset(); + } + + // --- languagesList --- + + public function testLanguagesListReturnsArray(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn([['id' => 'pl', 'name' => 'Polski']]); + + $repo = new LanguagesRepository($db); + $this->assertSame([['id' => 'pl', 'name' => 'Polski']], $repo->languagesList()); + } + + public function testLanguagesListReturnsEmptyWhenNull(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn(null); + + $repo = new LanguagesRepository($db); + $this->assertSame([], $repo->languagesList()); + } + + // --- languageDetails --- + + public function testLanguageDetailsReturnsRowWhenFound(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(['id' => 'pl', 'name' => 'Polski']); + + $repo = new LanguagesRepository($db); + $this->assertSame('pl', $repo->languageDetails('pl')['id']); + } + + public function testLanguageDetailsReturnsNullWhenNotFound(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(null); + + $repo = new LanguagesRepository($db); + $this->assertNull($repo->languageDetails('xx')); + } + + // --- activeLanguages --- + + public function testActiveLanguagesQueriesDbAndCaches(): void + { + $expected = [['id' => 'pl', 'name' => 'Polski', 'domain' => null]]; + $db = $this->mockDb(); + $db->expects($this->once())->method('select')->willReturn($expected); + + $repo = new LanguagesRepository($db); + $this->assertSame($expected, $repo->activeLanguages()); + // Drugi odczyt — z cache (mock select nie zostanie wywołany drugi raz) + $this->assertSame($expected, $repo->activeLanguages()); + } + + public function testActiveLanguagesReturnsEmptyWhenNull(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn(null); + + $repo = new LanguagesRepository($db); + $this->assertSame([], $repo->activeLanguages()); + } + + // --- maxOrder --- + + public function testMaxOrderReturnsInteger(): void + { + $db = $this->mockDb(); + $db->method('max')->willReturn('5'); + + $repo = new LanguagesRepository($db); + $this->assertSame(5, $repo->maxOrder()); + } + + // --- translationDelete --- + + public function testTranslationDeleteReturnsTrueOnSuccess(): void + { + $db = $this->mockDb(); + $db->method('delete')->willReturn(1); + + $repo = new LanguagesRepository($db); + $this->assertTrue($repo->translationDelete(1)); + } + + public function testTranslationDeleteReturnsFalseOnFailure(): void + { + $db = $this->mockDb(); + $db->method('delete')->willReturn(0); + + $repo = new LanguagesRepository($db); + $this->assertFalse($repo->translationDelete(1)); + } + + // --- translationDetails --- + + public function testTranslationDetailsReturnsRowOrNull(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(['id' => 1, 'text' => 'hello']); + + $repo = new LanguagesRepository($db); + $this->assertSame(['id' => 1, 'text' => 'hello'], $repo->translationDetails(1)); + } +} diff --git a/tests/Unit/Domain/Settings/SettingsRepositoryTest.php b/tests/Unit/Domain/Settings/SettingsRepositoryTest.php new file mode 100644 index 0000000..349f975 --- /dev/null +++ b/tests/Unit/Domain/Settings/SettingsRepositoryTest.php @@ -0,0 +1,101 @@ +createMock(\medoo::class); + } + + protected function setUp(): void + { + \Shared\Cache\CacheHandler::reset(); + } + + // --- allSettings --- + + public function testAllSettingsReturnsMappedArray(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn([ + ['param' => 'site_name', 'value' => 'Test CMS'], + ['param' => 'email', 'value' => 'admin@test.pl'], + ]); + + $repo = new SettingsRepository($db); + $result = $repo->allSettings(); + + $this->assertSame('Test CMS', $result['site_name']); + $this->assertSame('admin@test.pl', $result['email']); + } + + public function testAllSettingsReturnsEmptyArrayWhenDbReturnsNull(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn(null); + + $repo = new SettingsRepository($db); + $this->assertSame([], $repo->allSettings()); + } + + public function testAllSettingsUsesCache(): void + { + $db = $this->mockDb(); + $db->expects($this->never())->method('select'); + + \CacheHandlerStub::store('settings_details', ['cached' => '1']); + + $repo = new SettingsRepository($db); + $result = $repo->allSettings(); + + $this->assertSame('1', $result['cached']); + } + + // --- update --- + + public function testUpdateCallsDbUpdateWhenParamExists(): void + { + $db = $this->mockDb(); + $db->method('count')->willReturn(1); + $db->expects($this->once())->method('update')->willReturn(true); + $db->expects($this->never())->method('insert'); + + $repo = new SettingsRepository($db); + $this->assertTrue($repo->update('site_name', 'Nowa Nazwa')); + } + + public function testUpdateCallsDbInsertWhenParamMissing(): void + { + $db = $this->mockDb(); + $db->method('count')->willReturn(0); + $db->expects($this->once())->method('insert')->willReturn(true); + $db->expects($this->never())->method('update'); + + $repo = new SettingsRepository($db); + $repo->update('new_param', 'value'); + } + + // --- visitCounter --- + + public function testVisitCounterReturnsValue(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn('1234'); + + $repo = new SettingsRepository($db); + $this->assertSame('1234', $repo->visitCounter()); + } + + public function testVisitCounterReturnsNullWhenEmpty(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(null); + + $repo = new SettingsRepository($db); + $this->assertNull($repo->visitCounter()); + } +} diff --git a/tests/Unit/Domain/User/UserRepositoryTest.php b/tests/Unit/Domain/User/UserRepositoryTest.php new file mode 100644 index 0000000..b2b47c6 --- /dev/null +++ b/tests/Unit/Domain/User/UserRepositoryTest.php @@ -0,0 +1,252 @@ +createMock(\medoo::class); + } + + protected function setUp(): void + { + \Shared\Cache\CacheHandler::reset(); + } + + // --- find --- + + public function testFindReturnsUserArray(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(['id' => 1, 'login' => 'admin']); + + $repo = new UserRepository($db); + $this->assertSame('admin', $repo->find(1)['login']); + } + + public function testFindReturnsNullWhenNotFound(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(null); + + $repo = new UserRepository($db); + $this->assertNull($repo->find(99)); + } + + // --- findByLogin --- + + public function testFindByLoginReturnsUser(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(['id' => 1, 'login' => 'admin']); + + $repo = new UserRepository($db); + $this->assertNotNull($repo->findByLogin('admin')); + } + + // --- all --- + + public function testAllReturnsArray(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn([['id' => 1], ['id' => 2]]); + + $repo = new UserRepository($db); + $this->assertCount(2, $repo->all()); + } + + public function testAllReturnsEmptyArrayWhenNull(): void + { + $db = $this->mockDb(); + $db->method('select')->willReturn(null); + + $repo = new UserRepository($db); + $this->assertSame([], $repo->all()); + } + + // --- hasPrivilege --- + + public function testHasPrivilegeReturnsTrueForAdminUser(): void + { + $db = $this->mockDb(); + $repo = new UserRepository($db); + // userId === 1 zawsze ma uprawnienia, bez zapytania do DB + $db->expects($this->never())->method('count'); + $this->assertTrue($repo->hasPrivilege('articles', 1)); + } + + public function testHasPrivilegeReturnsTrueWhenPrivilegeExists(): void + { + $db = $this->mockDb(); + $db->method('count')->willReturn(1); + + $repo = new UserRepository($db); + $this->assertTrue($repo->hasPrivilege('articles', 2)); + } + + public function testHasPrivilegeReturnsFalseWhenPrivilegeMissing(): void + { + $db = $this->mockDb(); + $db->method('count')->willReturn(0); + + $repo = new UserRepository($db); + $this->assertFalse($repo->hasPrivilege('articles', 2)); + } + + // --- logon --- + + public function testLogonReturnsZeroWhenUserNotFound(): void + { + $db = $this->mockDb(); + // Pierwsze get() (sprawdź czy login istnieje) → null + $db->method('get')->willReturn(null); + + $repo = new UserRepository($db); + $this->assertSame(0, $repo->logon('unknown', 'pass')); + } + + public function testLogonReturnsMinusOneWhenAccountBlocked(): void + { + $db = $this->mockDb(); + // Pierwsze get() → użytkownik istnieje, drugie → konto zablokowane (null) + $db->method('get')->willReturnOnConsecutiveCalls( + ['id' => 2, 'login' => 'user'], + null + ); + + $repo = new UserRepository($db); + $this->assertSame(-1, $repo->logon('user', 'pass')); + } + + public function testLogonReturnsOneOnSuccess(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturnOnConsecutiveCalls( + ['id' => 2, 'login' => 'user'], // login istnieje + ['id' => 2, 'status' => 1, 'error_logged_count' => 0], // nie zablokowany + ['id' => 2] // hasło poprawne + ); + $db->method('update')->willReturn(true); + + $repo = new UserRepository($db); + $this->assertSame(1, $repo->logon('user', 'pass')); + } + + // --- isLoginTaken --- + + public function testIsLoginTakenReturnsTrueWhenExists(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn('user'); + + $repo = new UserRepository($db); + $this->assertTrue($repo->isLoginTaken('user')); + } + + public function testIsLoginTakenReturnsFalseWhenFree(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(null); + + $repo = new UserRepository($db); + $this->assertFalse($repo->isLoginTaken('newuser')); + } + + // --- verifyTwofaCode --- + + public function testVerifyTwofaCodeReturnsFalseWhenUserNotFound(): void + { + $db = $this->mockDb(); + $db->method('get')->willReturn(null); + + $repo = new UserRepository($db); + $this->assertFalse($repo->verifyTwofaCode(1, '123456')); + } + + public function testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts(): void + { + $db = $this->mockDb(); + $user = [ + 'id' => 2, + 'twofa_failed_attempts' => 5, + 'twofa_expires_at' => date('Y-m-d H:i:s', time() + 600), + 'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT), + ]; + $db->method('get')->willReturn($user); + + $repo = new UserRepository($db); + $this->assertFalse($repo->verifyTwofaCode(2, '123456')); + } + + public function testVerifyTwofaCodeReturnsFalseWhenExpired(): void + { + $db = $this->mockDb(); + $user = [ + 'id' => 2, + 'twofa_failed_attempts' => 0, + 'twofa_expires_at' => date('Y-m-d H:i:s', time() - 1), + 'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT), + ]; + $db->method('get')->willReturn($user); + $db->method('update')->willReturn(true); + + $repo = new UserRepository($db); + $this->assertFalse($repo->verifyTwofaCode(2, '123456')); + } + + public function testVerifyTwofaCodeReturnsTrueOnValidCode(): void + { + $code = '123456'; + $db = $this->mockDb(); + $user = [ + 'id' => 2, + 'twofa_failed_attempts' => 0, + 'twofa_expires_at' => date('Y-m-d H:i:s', time() + 600), + 'twofa_code_hash' => password_hash($code, PASSWORD_DEFAULT), + ]; + // find() wywołuje get() dwa razy (raz przez verifyTwofaCode, raz przez update) + $db->method('get')->willReturn($user); + $db->method('update')->willReturn(true); + + $repo = new UserRepository($db); + $this->assertTrue($repo->verifyTwofaCode(2, $code)); + } + + // --- delete --- + + public function testDeleteReturnsTrueOnSuccess(): void + { + $db = $this->mockDb(); + $db->method('delete')->willReturn(1); + + $repo = new UserRepository($db); + $this->assertTrue($repo->delete(2)); + } + + // --- save — walidacja --- + + public function testSaveReturnsErrorWhenPasswordTooShort(): void + { + $db = $this->mockDb(); + $db->method('delete')->willReturn(1); + + $repo = new UserRepository($db); + $result = $repo->save(0, 'newuser', 'on', '', '123', '123', 0, []); + + $this->assertSame('error', $result['status']); + } + + public function testSaveReturnsErrorWhenPasswordsMismatch(): void + { + $db = $this->mockDb(); + $db->method('delete')->willReturn(1); + + $repo = new UserRepository($db); + $result = $repo->save(0, 'newuser', 'on', '', 'password1', 'password2', 0, []); + + $this->assertSame('error', $result['status']); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..cc85cf5 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,51 @@ +