createMock(\medoo::class); $mockDb->expects($this->once()) ->method('max') ->with('pp_shop_attributes', 'o') ->willReturn(7); $repository = new AttributeRepository($mockDb); $result = $repository->findAttribute(0); $this->assertSame(0, (int)$result['id']); $this->assertSame(1, (int)$result['status']); $this->assertSame(0, (int)$result['type']); $this->assertSame(8, (int)$result['o']); $this->assertSame([], $result['languages']); } public function testListForAdminWhitelistsSortDirectionAndPerPage(): void { $mockDb = $this->createMock(\medoo::class); $queries = []; $mockDb->method('select') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_langs') { return [['id' => 'pl', 'start' => 1, 'o' => 1]]; } return []; }); $mockDb->method('query') ->willReturnCallback(function ($sql, $params = []) use (&$queries) { $queries[] = ['sql' => $sql, 'params' => $params]; if (preg_match('/SELECT\s+COUNT\(0\)\s+FROM\s+pp_shop_attributes\s+AS\s+sa/i', $sql)) { return new class { public function fetchAll(): array { return [[1]]; } }; } return new class { public function fetchAll(): array { return [[ 'id' => '10', 'status' => '1', 'type' => '2', 'o' => '3', 'name_default' => '', 'name_any' => 'Wzor A', 'values_count' => '5', 'name_for_sort' => 'Wzor A', ]]; } }; }); $repository = new AttributeRepository($mockDb); $result = $repository->listForAdmin([], 'id DESC; DROP TABLE pp_shop_attributes; --', 'DESC; DELETE', 1, 999); $this->assertCount(2, $queries); $dataSql = $queries[1]['sql']; $this->assertMatchesRegularExpression('/ORDER BY\s+sa\.o\s+ASC,\s+sa\.id\s+ASC/i', $dataSql); $this->assertStringNotContainsString('DROP TABLE', $dataSql); $this->assertStringNotContainsString('DELETE', $dataSql); $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); $this->assertSame('Wzor A', $result['items'][0]['name']); $this->assertSame(5, (int)$result['items'][0]['values_count']); } public function testSaveValuesRemovesObsoleteRowsAndSetsDefault(): void { $mockDb = $this->createMock(\medoo::class); $insertCalls = []; $updateCalls = []; $deleteCalls = []; $mockDb->method('select') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_attributes_values' && $columns === 'id') { return [10, 11]; } if ($table === 'pp_shop_products_attributes') { return []; } return []; }); $mockDb->method('count') ->willReturnCallback(function ($table, $where) { if ($table === 'pp_shop_attributes_values' && (int)($where['AND']['id'] ?? 0) === 11) { return 1; } return 0; }); $mockDb->method('get') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_attributes_values_langs') { return null; } return null; }); $mockDb->method('insert') ->willReturnCallback(function ($table, $row) use (&$insertCalls) { $insertCalls[] = ['table' => $table, 'row' => $row]; }); $mockDb->expects($this->once()) ->method('id') ->willReturn(22); $mockDb->method('update') ->willReturnCallback(function ($table, $row, $where) use (&$updateCalls) { $updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where]; return true; }); $mockDb->method('delete') ->willReturnCallback(function ($table, $where) use (&$deleteCalls) { $deleteCalls[] = ['table' => $table, 'where' => $where]; return true; }); $repository = new AttributeRepository($mockDb); $saved = $repository->saveValues(3, [ 'rows' => [ [ 'id' => 11, 'is_default' => false, 'impact_on_the_price' => '', 'translations' => [ 'pl' => ['name' => 'Niebieski', 'value' => 'blue'], ], ], [ 'id' => 0, 'is_default' => true, 'impact_on_the_price' => null, 'translations' => [ 'pl' => ['name' => 'Czerwony', 'value' => 'red'], ], ], ], ]); $this->assertTrue($saved); $this->assertTrue($this->hasDeleteCall($deleteCalls, 'pp_shop_attributes_values_langs', ['value_id' => 10])); $this->assertTrue($this->hasDeleteCall($deleteCalls, 'pp_shop_attributes_values', ['id' => 10])); $this->assertTrue($this->hasUpdateCall($updateCalls, 'pp_shop_attributes_values', ['is_default' => 0], ['attribute_id' => 3])); $this->assertTrue($this->hasUpdateCall($updateCalls, 'pp_shop_attributes_values', ['is_default' => 1], ['id' => 22])); $this->assertTrue($this->hasInsertInto($insertCalls, 'pp_shop_attributes_values')); $this->assertTrue($this->hasInsertInto($insertCalls, 'pp_shop_attributes_values_langs')); } public function testSaveValuesDeletesTranslationWhenNameIsEmpty(): void { $mockDb = $this->createMock(\medoo::class); $deleteCalls = []; $mockDb->method('select') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_attributes_values' && $columns === 'id') { return [5]; } if ($table === 'pp_shop_products_attributes') { return []; } return []; }); $mockDb->method('count')->willReturn(1); $mockDb->method('get') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_attributes_values_langs' && $columns === 'id') { return 77; } return null; }); $mockDb->method('update')->willReturn(true); $mockDb->method('delete') ->willReturnCallback(function ($table, $where) use (&$deleteCalls) { $deleteCalls[] = ['table' => $table, 'where' => $where]; return true; }); $repository = new AttributeRepository($mockDb); $saved = $repository->saveValues(9, [ 'rows' => [ [ 'id' => 5, 'is_default' => true, 'impact_on_the_price' => null, 'translations' => [ 'pl' => ['name' => '', 'value' => ''], ], ], ], ]); $this->assertTrue($saved); $this->assertTrue($this->hasDeleteCall($deleteCalls, 'pp_shop_attributes_values_langs', ['id' => 77])); } public function testGetAttributeValueByIdUsesDefaultLanguageWhenNotProvided(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('select') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_langs') { return [['id' => 'pl', 'start' => 1, 'o' => 1]]; } return []; }); $mockDb->expects($this->once()) ->method('get') ->with( 'pp_shop_attributes_values_langs', 'name', ['AND' => ['value_id' => 123, 'lang_id' => 'pl']] ) ->willReturn('Czerwony'); $repository = new AttributeRepository($mockDb); $result = $repository->getAttributeValueById(123); $this->assertSame('Czerwony', $result); } // ── Frontend methods tests ────────────────────────────────── public function testFrontAttributeDetailsReturnsAttributeWithLanguage(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->expects($this->exactly(2)) ->method('get') ->willReturnOnConsecutiveCalls( ['id' => 5, 'status' => 1, 'type' => 0, 'o' => 2], ['lang_id' => 'pl', 'name' => 'Kolor'] ); $repository = new AttributeRepository($mockDb); $result = $repository->frontAttributeDetails(5, 'pl'); $this->assertIsArray($result); $this->assertSame(5, (int)$result['id']); $this->assertSame('Kolor', $result['language']['name']); $this->assertSame('pl', $result['language']['lang_id']); } public function testFrontAttributeDetailsReturnsFallbackForNotFound(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('get')->willReturn(null); $repository = new AttributeRepository($mockDb); $result = $repository->frontAttributeDetails(999, 'pl'); $this->assertIsArray($result); $this->assertSame(999, (int)$result['id']); $this->assertSame(0, (int)$result['status']); $this->assertSame('pl', $result['language']['lang_id']); $this->assertSame('', $result['language']['name']); } public function testFrontValueDetailsReturnsValueWithLanguage(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->expects($this->exactly(2)) ->method('get') ->willReturnOnConsecutiveCalls( ['id' => 12, 'attribute_id' => 5, 'is_default' => 1, 'impact_on_the_price' => null], ['lang_id' => 'pl', 'name' => 'Czerwony'] ); $repository = new AttributeRepository($mockDb); $result = $repository->frontValueDetails(12, 'pl'); $this->assertIsArray($result); $this->assertSame(12, (int)$result['id']); $this->assertSame('Czerwony', $result['language']['name']); $this->assertSame('pl', $result['language']['lang_id']); } public function testFrontValueDetailsReturnsFallbackForNotFound(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('get')->willReturn(null); $repository = new AttributeRepository($mockDb); $result = $repository->frontValueDetails(999, 'en'); $this->assertIsArray($result); $this->assertSame(999, (int)$result['id']); $this->assertSame('en', $result['language']['lang_id']); $this->assertSame('', $result['language']['name']); } private function hasDeleteCall(array $calls, string $table, array $where): bool { foreach ($calls as $call) { if ($call['table'] === $table && $call['where'] == $where) { return true; } } return false; } private function hasUpdateCall(array $calls, string $table, array $row, array $where): bool { foreach ($calls as $call) { if ($call['table'] === $table && $call['row'] == $row && $call['where'] == $where) { return true; } } return false; } private function hasInsertInto(array $calls, string $table): bool { foreach ($calls as $call) { if ($call['table'] === $table) { return true; } } return false; } // --- listForApi --- public function testListForApiReturnsActiveAttributesWithValues(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('select') ->willReturnCallback(function ($table, $columns, $where) { if ($table === 'pp_shop_attributes') { return [ ['id' => '5', 'type' => '0', 'status' => '1'], ]; } if ($table === 'pp_shop_attributes_langs') { return [ ['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar'], ['attribute_id' => '5', 'lang_id' => 'en', 'name' => 'Size'], ]; } if ($table === 'pp_shop_attributes_values') { return [ ['id' => '12', 'attribute_id' => '5', 'is_default' => '1', 'impact_on_the_price' => null], ['id' => '13', 'attribute_id' => '5', 'is_default' => '0', 'impact_on_the_price' => '10.00'], ]; } if ($table === 'pp_shop_attributes_values_langs') { return [ ['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M'], ['value_id' => '12', 'lang_id' => 'en', 'name' => 'M'], ['value_id' => '13', 'lang_id' => 'pl', 'name' => 'L'], ['value_id' => '13', 'lang_id' => 'en', 'name' => 'L'], ]; } if ($table === 'pp_langs') { return [['id' => 'pl', 'start' => 1, 'o' => 1]]; } return []; }); $repository = new AttributeRepository($mockDb); $result = $repository->listForApi(); $this->assertCount(1, $result); $this->assertSame(5, $result[0]['id']); $this->assertSame(0, $result[0]['type']); $this->assertSame(1, $result[0]['status']); $this->assertSame('Rozmiar', $result[0]['names']['pl']); $this->assertSame('Size', $result[0]['names']['en']); $this->assertCount(2, $result[0]['values']); $this->assertSame(12, $result[0]['values'][0]['id']); $this->assertSame(1, $result[0]['values'][0]['is_default']); $this->assertNull($result[0]['values'][0]['impact_on_the_price']); $this->assertSame(13, $result[0]['values'][1]['id']); $this->assertSame(10.0, $result[0]['values'][1]['impact_on_the_price']); } public function testListForApiReturnsEmptyWhenNoAttributes(): void { $mockDb = $this->createMock(\medoo::class); $mockDb->method('select') ->willReturnCallback(function ($table) { if ($table === 'pp_shop_attributes') { return []; } if ($table === 'pp_langs') { return [['id' => 'pl', 'start' => 1, 'o' => 1]]; } return []; }); $repository = new AttributeRepository($mockDb); $result = $repository->listForApi(); $this->assertSame([], $result); } }