createMock(\medoo::class); $repository = new PromotionRepository($mockDb); $result = $repository->find(0); $this->assertIsArray($result); $this->assertSame(0, (int)$result['id']); $this->assertSame(1, (int)$result['status']); $this->assertNull($result['date_from']); $this->assertSame([], $result['categories']); $this->assertSame([], $result['condition_categories']); } public function testSaveInsertsPromotionAndReturnsId(): void { $mockDb = $this->createMock(\medoo::class); $insertRow = null; $mockDb->expects($this->once()) ->method('insert') ->willReturnCallback(function ($table, $row) use (&$insertRow) { $this->assertSame('pp_shop_promotion', $table); $this->assertArrayHasKey('name', $row); $insertRow = $row; }); $mockDb->expects($this->once()) ->method('id') ->willReturn(123); $repository = new PromotionRepository($mockDb); $id = $repository->save([ 'name' => 'Promocja testowa', 'status' => 'on', 'condition_type' => 1, 'discount_type' => 1, 'amount' => '10', 'date_from' => '2026-02-01', 'categories' => [1, 2], ]); $this->assertSame(123, $id); $this->assertIsArray($insertRow); $this->assertSame('2026-02-01', $insertRow['date_from'] ?? null); } public function testDeleteReturnsFalseForInvalidId(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->expects($this->never())->method('delete'); $repository = new PromotionRepository($mockDb); $this->assertFalse($repository->delete(0)); } public function testDeleteReturnsTrueWhenDatabaseDeleteSucceeds(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->expects($this->once()) ->method('delete') ->with('pp_shop_promotion', ['id' => 55]) ->willReturn(true); $repository = new PromotionRepository($mockDb); $this->assertTrue($repository->delete(55)); } public function testListForAdminWhitelistsSortAndDirection(): void { $mockDb = $this->createMock(\medoo::class); $queries = []; $mockDb->method('query') ->willReturnCallback(function ($sql, $params = []) use (&$queries) { $queries[] = ['sql' => $sql, 'params' => $params]; if (strpos($sql, 'COUNT(0)') !== false) { return new class { public function fetchAll() { return [[1]]; } }; } return new class { public function fetchAll() { return [[ 'id' => 1, 'name' => 'Promo', 'status' => 1, 'condition_type' => 1, 'date_to' => null, ]]; } }; }); $repository = new PromotionRepository($mockDb); $repository->listForAdmin( [], 'date_to DESC; DROP TABLE pp_shop_promotion; --', 'DESC; DELETE FROM pp_users; --', 1, 500 ); $this->assertCount(2, $queries); $dataSql = $queries[1]['sql']; $this->assertMatchesRegularExpression('/ORDER BY\s+sp\.id\s+DESC,\s+sp\.id\s+DESC/i', $dataSql); $this->assertStringNotContainsString('DROP TABLE', $dataSql); $this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql); $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); } public function testCategoriesTreeReturnsHierarchy(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('select') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_categories' && array_key_exists('parent_id', $where)) { if ($where['parent_id'] === null) { return [['id' => 10]]; } if ((int)$where['parent_id'] === 10) { return [['id' => 11]]; } return []; } if ($table === 'pp_shop_categories_langs') { if ((int)$where['category_id'] === 10) { return [['lang_id' => 'pl', 'title' => 'Kategoria A']]; } if ((int)$where['category_id'] === 11) { return [['lang_id' => 'pl', 'title' => 'Podkategoria A1']]; } return []; } if ($table === 'pp_langs') { return [['id' => 'pl', 'start' => 1, 'o' => 1]]; } return []; }); $mockDb->method('get') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_categories') { $id = (int)$where['id']; return ['id' => $id, 'status' => 1]; } return null; }); $repository = new PromotionRepository($mockDb); $tree = $repository->categoriesTree(null); $this->assertCount(1, $tree); $this->assertSame(10, (int)$tree[0]['id']); $this->assertSame('Kategoria A', $tree[0]['title']); $this->assertCount(1, $tree[0]['subcategories']); $this->assertSame(11, (int)$tree[0]['subcategories'][0]['id']); } // ========================================================================= // Frontend: basket promotion logic (migrated from front\factory\ShopPromotion) // ========================================================================= private function mockPromotion(array $data): object { return new class($data) { private $data; public function __construct($data) { $this->data = $data; } public function __get($key) { return isset($this->data[$key]) ? $this->data[$key] : null; } }; } private function makeBasket(array $items): array { $basket = []; foreach ($items as $i => $item) { $basket[$i] = array_merge(['product-id' => $item['id']], $item); } return $basket; } /** * Test applyTypeWholeBasket — rabat na cały koszyk */ public function testApplyTypeWholeBasketAppliesDiscountToAll(): void { $mockDb = $this->createMock(\medoo::class); // productCategoriesFront zwraca kategorie $mockDb->method('get')->willReturn(null); // parent_id = null $mockStmt = $this->createMock(\PDOStatement::class); $mockStmt->method('fetchAll')->willReturn([['category_id' => 1]]); $mockDb->method('query')->willReturn($mockStmt); $promotion = $this->mockPromotion([ 'discount_type' => 1, 'amount' => 10, 'include_coupon' => 0, 'include_product_promo' => 0, ]); $basket = $this->makeBasket([ ['id' => 1], ['id' => 2], ]); $repository = new PromotionRepository($mockDb); $result = $repository->applyTypeWholeBasket($basket, $promotion); $this->assertSame(1, $result[0]['discount_type']); $this->assertSame(10, $result[0]['discount_amount']); $this->assertSame(1, $result[1]['discount_type']); } /** * Test applyTypeCategoriesOr — rabat na produkty z kat. 1 lub 2 */ public function testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('get')->willReturn(null); $mockStmt = $this->createMock(\PDOStatement::class); $mockStmt->method('fetchAll')->willReturn([['category_id' => 5]]); $mockDb->method('query')->willReturn($mockStmt); $promotion = $this->mockPromotion([ 'categories' => json_encode([5]), 'condition_categories' => json_encode([10]), 'discount_type' => 1, 'amount' => 15, 'include_coupon' => 1, 'include_product_promo' => 0, ]); $basket = $this->makeBasket([['id' => 1]]); $repository = new PromotionRepository($mockDb); $result = $repository->applyTypeCategoriesOr($basket, $promotion); $this->assertSame(1, $result[0]['discount_type']); $this->assertSame(15, $result[0]['discount_amount']); $this->assertSame(1, $result[0]['discount_include_coupon']); } /** * Test applyTypeCategoryCondition — rabat na kat. I jeśli kat. II w koszyku */ public function testApplyTypeCategoryConditionAppliesWhenConditionMet(): void { $mockDb = $this->createMock(\medoo::class); $callCount = 0; $mockDb->method('get')->willReturn(null); $mockStmt1 = $this->createMock(\PDOStatement::class); $mockStmt1->method('fetchAll')->willReturnOnConsecutiveCalls( [['category_id' => 10]], // product 1 — condition category [['category_id' => 5]], // product 2 — target category [['category_id' => 10]], // product 1 — check for discount (not matching target) [['category_id' => 5]] // product 2 — check for discount (matching target) ); $mockDb->method('query')->willReturn($mockStmt1); $promotion = $this->mockPromotion([ 'categories' => json_encode([5]), 'condition_categories' => json_encode([10]), 'discount_type' => 1, 'amount' => 20, 'include_coupon' => 0, 'include_product_promo' => 0, ]); $basket = $this->makeBasket([ ['id' => 1], ['id' => 2], ]); $repository = new PromotionRepository($mockDb); $result = $repository->applyTypeCategoryCondition($basket, $promotion); // Produkt 2 (kat. 5) powinien mieć rabat $this->assertSame(1, $result[1]['discount_type']); $this->assertSame(20, $result[1]['discount_amount']); } /** * Test applyTypeCategoryCondition — brak rabatu gdy warunek niespełniony */ public function testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('get')->willReturn(null); $mockStmt = $this->createMock(\PDOStatement::class); $mockStmt->method('fetchAll')->willReturn([['category_id' => 99]]); // nie pasuje do condition_categories $mockDb->method('query')->willReturn($mockStmt); $promotion = $this->mockPromotion([ 'categories' => json_encode([5]), 'condition_categories' => json_encode([10]), 'discount_type' => 1, 'amount' => 20, 'include_coupon' => 0, 'include_product_promo' => 0, ]); $basket = $this->makeBasket([['id' => 1]]); $repository = new PromotionRepository($mockDb); $result = $repository->applyTypeCategoryCondition($basket, $promotion); $this->assertArrayNotHasKey('discount_type', $result[0]); } /** * Test applyTypeCategoriesAnd — rabat gdy oba warunki spełnione */ public function testApplyTypeCategoriesAndAppliesWhenBothConditionsMet(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('get')->willReturn(null); $mockStmt = $this->createMock(\PDOStatement::class); $mockStmt->method('fetchAll')->willReturnOnConsecutiveCalls( [['category_id' => 10]], // product 1 — condition_categories ✓ [['category_id' => 5]], // product 2 — categories ✓ (condition_2) [['category_id' => 10]], // product 1 — check categories ✓ (condition check) [['category_id' => 5]], // product 2 — check categories ✓ (condition check) [['category_id' => 10]], // product 1 — discount assignment [['category_id' => 5]] // product 2 — discount assignment ); $mockDb->method('query')->willReturn($mockStmt); $promotion = $this->mockPromotion([ 'categories' => json_encode([5]), 'condition_categories' => json_encode([10]), 'discount_type' => 1, 'amount' => 25, 'include_coupon' => 1, 'include_product_promo' => 0, ]); $basket = $this->makeBasket([ ['id' => 1], ['id' => 2], ]); $repository = new PromotionRepository($mockDb); $result = $repository->applyTypeCategoriesAnd($basket, $promotion); $this->assertSame(1, $result[0]['discount_type']); $this->assertSame(25, $result[0]['discount_amount']); $this->assertSame(1, $result[1]['discount_type']); } }