From 62f2d6da5500a2bf3e95532baeb714a5d91b96c4 Mon Sep 17 00:00:00 2001 From: ailkiv Date: Wed, 20 Aug 2025 12:44:45 +0000 Subject: [PATCH] chore: Added phpunit for the filtering function in Row2Mapper::findAll Signed-off-by: ailkiv --- tests/unit/Database/DatabaseTestCase.php | 69 ++++++- tests/unit/Db/Row2MapperTest.php | 231 +++++++++++++++++++++-- 2 files changed, 278 insertions(+), 22 deletions(-) diff --git a/tests/unit/Database/DatabaseTestCase.php b/tests/unit/Database/DatabaseTestCase.php index a4412307a3..8eae93c50d 100644 --- a/tests/unit/Database/DatabaseTestCase.php +++ b/tests/unit/Database/DatabaseTestCase.php @@ -193,7 +193,12 @@ protected function createTestColumn(int $tableId, array $data = []) { 'mandatory' => false, 'order_weight' => 0, 'number_prefix' => '', - 'number_suffix' => '' + 'number_suffix' => '', + 'text_default' => '', + 'number_default' => null, + 'datetime_default' => '', + 'selection_default' => '', + 'usergroup_default' => '', ]; $testIdent = $data['test_ident'] ?? null; @@ -318,27 +323,32 @@ protected function addCellsToRow(int $rowId, array $cellsData, array $columnMapp */ protected function insertCellData(int $rowId, int $columnId, $value): void { $qb = $this->connection->getQueryBuilder(); - $result = $qb->select('type') + $result = $qb->select('type', 'subtype') ->from('tables_columns') ->where($qb->expr()->eq('id', $qb->createNamedParameter($columnId))) ->executeQuery(); - $columnType = $result->fetchOne(); + $column = $result->fetch(); $result->closeCursor(); - if (!$columnType) { + if (!$column) { throw new \InvalidArgumentException("Column with ID $columnId not found"); } - $this->insertCellIntoTypeTable($rowId, $columnId, $value, $columnType); + $this->insertCellIntoTypeTable($rowId, $columnId, $value, $column['type'], $column['subtype']); } /** * Inserts cell data into the appropriate type-specific table */ - protected function insertCellIntoTypeTable(int $rowId, int $columnId, $value, string $columnType): void { + protected function insertCellIntoTypeTable(int $rowId, int $columnId, $value, string $columnType, string $columnSubtype): void { $tableName = 'tables_row_cells_' . $columnType; + // Handle selection type - convert values to IDs based on selection_options + if ($columnType === 'selection' && $columnSubtype !== 'check') { + $value = $this->convertSelectionValuesToIds($columnId, $value); + } + $qb = $this->connection->getQueryBuilder(); $qb->insert($tableName) ->setValue('row_id', $qb->createNamedParameter($rowId)) @@ -403,6 +413,53 @@ protected function extractTestIdentMapping(array $results): array { return $mapping; } + /** + * Converts selection values to IDs based on selection_options + */ + protected function convertSelectionValuesToIds(int $columnId, $value) { + // Get column configuration to find selection_options + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select('selection_options') + ->from('tables_columns') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($columnId))) + ->executeQuery(); + + $selectionOptions = $result->fetchOne(); + $result->closeCursor(); + + if (!$selectionOptions) { + throw new \InvalidArgumentException("Column with ID $columnId not found"); + } + + $selectionOptions = json_decode($selectionOptions, true); + + // Create mapping from label to id + $optionMapping = []; + foreach ($selectionOptions as $option) { + if (isset($option['label']) && isset($option['id'])) { + $optionMapping[$option['label']] = $option['id']; + } + } + + // Convert single value or array of values + if (is_array($value)) { + // Multiple selection - convert each value to ID and return as JSON + $convertedValues = []; + foreach ($value as $optionText) { + if (isset($optionMapping[$optionText])) { + $convertedValues[] = $optionMapping[$optionText]; + } + } + return json_encode($convertedValues); + } else { + // Single selection - convert to ID + if (isset($optionMapping[$value])) { + return $optionMapping[$value]; + } + return null; + } + } + /** * Gets ID by test_ident from creation results * @param array $results Array of creation results diff --git a/tests/unit/Db/Row2MapperTest.php b/tests/unit/Db/Row2MapperTest.php index 6b7442fa56..4aaa475afb 100644 --- a/tests/unit/Db/Row2MapperTest.php +++ b/tests/unit/Db/Row2MapperTest.php @@ -68,10 +68,15 @@ private function initializeTestData(): void { ['test_ident' => 'sort_test_table', 'title' => 'Comprehensive Sort Test Table'], [ ['test_ident' => 'name', 'title' => 'Name', 'type' => 'text'], + ['test_ident' => 'surname', 'title' => 'Surname', 'type' => 'text'], ['test_ident' => 'age', 'title' => 'Age', 'type' => 'number'], ['test_ident' => 'birthday', 'title' => 'Birthday', 'type' => 'datetime'], ['test_ident' => 'department', 'title' => 'Department', 'type' => 'text'], - ['test_ident' => 'score', 'title' => 'Score', 'type' => 'number'] + ['test_ident' => 'score', 'title' => 'Score', 'type' => 'number'], + ['test_ident' => 'status', 'title' => 'Status', 'type' => 'selection', 'subtype' => '', 'selection_options' => json_encode([['id' => 0, 'label' => 'Active'], ['id' => 1, 'label' => 'Inactive'], ['id' => 2, 'label' => 'Pending']])], + ['test_ident' => 'skills', 'title' => 'Skills', 'type' => 'selection', 'subtype' => 'multi', 'selection_options' => json_encode([['id' => 0, 'label' => 'PHP'], ['id' => 1, 'label' => 'JavaScript'], ['id' => 2, 'label' => 'SQL'], ['id' => 3, 'label' => 'Python'], ['id' => 4, 'label' => 'Java'], ['id' => 5, 'label' => 'React'], ['id' => 6, 'label' => 'Node.js'], ['id' => 7, 'label' => 'MongoDB'], ['id' => 8, 'label' => 'Docker'], ['id' => 9, 'label' => 'Management'], ['id' => 10, 'label' => 'Communication'], ['id' => 11, 'label' => 'Excel'], ['id' => 12, 'label' => 'Accounting'], ['id' => 13, 'label' => 'Analysis']])], + ['test_ident' => 'is_available', 'title' => 'Available', 'type' => 'selection', 'subtype' => 'check', 'selection_options' => ''], + ['test_ident' => 'experience_years', 'title' => 'Experience (Years)', 'type' => 'number'] ], [ [ @@ -80,10 +85,15 @@ private function initializeTestData(): void { 'created_at' => '2023-01-01 10:00:00', 'cells' => [ 'name' => 'Alice', + 'surname' => 'Thompson-Jones', 'age' => 28, 'birthday' => '1995-05-15 10:30:00', 'department' => 'IT', - 'score' => 85.5 + 'score' => 85.5, + 'status' => 'Active', + 'skills' => ['PHP', 'JavaScript', 'SQL', 'Python'], + 'is_available' => '"true"', + 'experience_years' => 5 ] ], [ @@ -92,10 +102,15 @@ private function initializeTestData(): void { 'created_at' => '2023-01-02 11:00:00', 'cells' => [ 'name' => 'Bob', + 'surname' => 'Thompson', 'age' => 32, 'birthday' => '1991-12-03 14:20:00', 'department' => 'HR', - 'score' => 92.0 + 'score' => 92.0, + 'status' => 'Inactive', + 'skills' => ['Management', 'Communication'], + 'is_available' => '"false"', + // 'experience_years' - skipped intentionally for testing ] ], [ @@ -104,10 +119,15 @@ private function initializeTestData(): void { 'created_at' => '2023-01-03 12:00:00', 'cells' => [ 'name' => 'Charlie', + 'surname' => 'Wilson', 'age' => 25, 'birthday' => '1998-01-20 08:45:00', 'department' => 'IT', - 'score' => 78.3 + 'score' => 78.3, + 'status' => 'Active', + 'skills' => ['Python'], + 'is_available' => '"true"', + 'experience_years' => 2 ] ], [ @@ -119,7 +139,11 @@ private function initializeTestData(): void { 'age' => 25, 'birthday' => '1998-08-10 16:00:00', 'department' => 'Finance', - 'score' => 88.7 + 'score' => 88.7, + 'status' => 'Pending', + 'skills' => ['Excel', 'Accounting', 'Analysis'], + 'is_available' => '"false"', + 'experience_years' => 3 ] ], [ @@ -128,10 +152,14 @@ private function initializeTestData(): void { 'created_at' => '2023-01-05 14:00:00', 'cells' => [ 'name' => 'Eve', + 'surname' => 'Davis', 'age' => 30, 'birthday' => '1993-03-25 12:15:00', 'department' => 'IT', - 'score' => 95.2 + 'score' => 95.2, + 'status' => 'Active', + 'skills' => ['React', 'Node.js', 'MongoDB', 'Docker'], + 'experience_years' => 7 ] ] ] @@ -170,12 +198,12 @@ public static function sortingDataProvider(): array { ], 'Number column (Score) ASC' => [ [['columnId' => 'score', 'mode' => 'ASC']], - ['Charlie', 'Alice', 'Diana', 'Bob', 'Eve'], // Scores: 78.3, 85.5, 88.7, 92.0, 95.2 + ['Charlie', 'Alice', 'Diana', 'Bob', 'Eve'], // Scores: empty, 78.3, 85.5, 88.7, 95.2 'Sort by Score ascending' ], 'Number column (Score) DESC' => [ [['columnId' => 'score', 'mode' => 'DESC']], - ['Eve', 'Bob', 'Diana', 'Alice', 'Charlie'], // Scores: 95.2, 92.0, 88.7, 85.5, 78.3 + ['Eve', 'Bob', 'Diana', 'Alice', 'Charlie'], // Scores: 95.2, 92.0, 88.7, 85.5, 78.3, empty 'Sort by Score descending' ], 'DateTime column ASC' => [ @@ -217,6 +245,133 @@ public static function sortingDataProvider(): array { ]; } + /** + * Data provider for filtering tests + */ + public static function filteringDataProvider(): array { + return [ + // Text column + 'Surname contains "son"' => [ + [[['columnId' => 'surname', 'operator' => 'contains', 'value' => 'son']]], + ['Alice', 'Bob', 'Charlie'], // People with surnames Thompson-Jones, Thompson, Wilson + 'Filter by surname containing "son" - should include 3 rows, exclude 2' + ], + 'Surname begins with "Th"' => [ + [[['columnId' => 'surname', 'operator' => 'begins-with', 'value' => 'Th']]], + ['Alice', 'Bob'], // People with surnames Thompson-Jones, Thompson + 'Filter by surname starting with "Th" - should include 2 rows, exclude 3' + ], + 'Surname ends with "son"' => [ + [[['columnId' => 'surname', 'operator' => 'ends-with', 'value' => 'son']]], + ['Bob', 'Charlie'], // People with surnames Thompson, Wilson (Thompson-Jones doesn't end with "son") + 'Filter by surname ending with "son" - should include 2 rows, exclude 3' + ], + 'Surname is equal to "Thompson"' => [ + [[['columnId' => 'surname', 'operator' => 'is-equal', 'value' => 'Thompson']]], + ['Bob'], // Person with surname Thompson + 'Filter by surname exactly equal to "Thompson" - should include 1 row, exclude 4' + ], + 'Surname is empty' => [ + [[['columnId' => 'surname', 'operator' => 'is-empty', 'value' => '']]], + ['Diana'], // Person with empty surname + 'Filter by surname is empty - should include 1 row, exclude 4' + ], + + // Number column (Experience Years) + 'Experience years is equal to 5' => [ + [[['columnId' => 'experience_years', 'operator' => 'is-equal', 'value' => 5]]], + ['Alice'], // Person with experience exactly 5 years + 'Filter by experience years equal to 5 - should include 1 row, exclude 4' + ], + 'Experience years is greater than 5' => [ + [[['columnId' => 'experience_years', 'operator' => 'is-greater-than', 'value' => 5]]], + ['Eve'], // Person with experience 7 years + 'Filter by experience years greater than 5 - should include 1 rows, exclude 4' + ], + 'Experience years is greater than or equal to 5' => [ + [[['columnId' => 'experience_years', 'operator' => 'is-greater-than-or-equal', 'value' => 5]]], + ['Alice', 'Eve'], // Person with experience 5, 7 years + 'Filter by experience years greater than or equal to 5 - should include 2 rows, exclude 3' + ], + 'Experience years is lower than 5' => [ + [[['columnId' => 'experience_years', 'operator' => 'is-lower-than', 'value' => 5]]], + ['Bob', 'Charlie', 'Diana'], // Person with experience 2, 3 years or undefined + 'Filter by experience years lower than 5 - should include 3 rows, exclude 2' + ], + 'Experience years is lower than or equal to 5' => [ + [[['columnId' => 'experience_years', 'operator' => 'is-lower-than-or-equal', 'value' => 5]]], + ['Alice', 'Bob', 'Charlie', 'Diana'], // Person with experience 2, 3, 5 years or undefined + 'Filter by experience years lower than or equal to 5 - should include 4 rows, exclude 1' + ], + 'Experience years is empty' => [ + [[['columnId' => 'experience_years', 'operator' => 'is-empty', 'value' => '']]], + ['Bob'], // Person with empty experience years + 'Filter by experience years is empty - should include 1 rows, exclude 4' + ], + + // Selection-check column + 'Available is equal to true' => [ + [[['columnId' => 'is_available', 'operator' => 'is-equal', 'value' => '@checked']]], + ['Alice', 'Charlie'], // Person with is_available = "true" + 'Filter by is_available equal to "true" - should include 2 rows, exclude 3' + ], + 'Available is equal to false' => [ + [[['columnId' => 'is_available', 'operator' => 'is-equal', 'value' => '@unchecked']]], + ['Bob', 'Diana'], // Person with is_available = "false" + 'Filter by is_available equal to "false" - should include 2 rows, exclude 3' + ], + 'Available is empty' => [ + [[['columnId' => 'is_available', 'operator' => 'is-empty', 'value' => '']]], + ['Eve'], // Person with empty is_available + 'Filter by is_available is empty - should include 1 row, exclude 4' + ], + + // Selection-single column (Status) + 'Status is equal to "Inactive"' => [ + [[['columnId' => 'status', 'operator' => 'is-equal', 'value' => '@selection-id-1']]], + ['Bob'], // Person with status "Inactive" (id: 1) + 'Filter by status exactly equal to "Inactive" - should include 1 row, exclude 4' + ], + 'Status contains "Active"' => [ + [[['columnId' => 'status', 'operator' => 'contains', 'value' => '@selection-id-0']]], + ['Alice', 'Charlie', 'Eve'], // Person with status "Active" (id: 0) + 'Filter by status containing "Active" - should include 3 rows, exclude 2' + ], + /* currently work only frontend + 'Status contains "Active"' => [ + [[['columnId' => 'status', 'operator' => 'contains', 'value' => 'Act']]], + ['Alice', 'Charlie', 'Eve'], // Person with status "Active" (id: 0) + 'Filter by status containing "Active" - should include 3 rows, exclude 2' + ],*/ + + // Selection-multiple column (Skills) + 'Skills contains "PHP"' => [ + [[['columnId' => 'skills', 'operator' => 'contains', 'value' => '@selection-id-0']]], + ['Alice'], // Person with skills "PHP" (id: 0) + 'Filter by skills containing "PHP" - should include 1 rows, exclude 4' + ], + 'Skills contains "Python"' => [ + [[['columnId' => 'skills', 'operator' => 'is-equal', 'value' => '@selection-id-3']]], + ['Charlie'], // Person with skills "Python" (id: 3) + 'Filter by skills containing "Python" - should include 1 rows, exclude 4' + ], + /* currently not work (frontend filter settings not support multiple values) + 'Skills is equal to "Management", "Communication"' => [ + [[['columnId' => 'skills', 'operator' => 'is-equal', 'value' => ['@selection-id-9', '@selection-id-10']]], + ['Bob'], // Person with skills "Management" (id: 9) and "Communication" (id: 10) + 'Filter by skills exactly equal to "Management" and "Communication" - should include 1 rows, exclude 4' + ],*/ + /* currently work only frontend + 'Skills contains "Pyt"' => [ + [[['columnId' => 'skills', 'operator' => 'contains', 'value' => 'Pyt']]], + ['Alice', 'Charlie'], // Person with skills "Python" (id: 3) + 'Filter by skills containing "Python" - should include 2 rows, exclude 3' + ],*/ + + + ]; + } + /** * @dataProvider sortingDataProvider */ @@ -242,6 +397,30 @@ public function testFindAllWithVariousSorting(array $sortWithNames, array $expec $this->assertEquals($expectedNameOrderLimited, $actualNameOrderLimited, "Failed sorting test with limit/offset: $description"); } + /** + * @dataProvider filteringDataProvider + */ + public function testFindAllWithVariousFilters(array $filtersWithNames, array $expectedNames, string $description): void { + $this->setupRealColumnMapper(self::$testTableId); + + // Convert column names to IDs for each filter group + $filters = []; + foreach ($filtersWithNames as $filterGroup) { + $filters[] = $this->convertFilterColumnNamesToIds($filterGroup); + } + + // Execute query with filtering + $rows = $this->mapper->findAll(self::$testColumnIds, self::$testTableId, null, null, $filters, null, 'test_user'); + + // Check that the correct number of rows were returned + $this->assertCount(count($expectedNames), $rows, "Should return correct number of rows for: $description"); + + // Check that the returned rows have the expected names (we filter by surname, but verify by name) + $nameColumnId = self::$testColumnIds[0]; // Name column is first + $actualNames = array_map(fn ($row) => $this->getCellValue($row, $nameColumnId), $rows); + $this->assertEquals($expectedNames, $actualNames, "Failed filtering test: $description"); + } + /** * Test for checking behavior with non-existent columnId */ @@ -279,7 +458,7 @@ public function testFindAllWithMixedExistingAndNonExistentColumns(): void { // Use mixed array: existing column + non-existent $mixedSort = [ - ['columnId' => self::$testColumnIds[1], 'mode' => 'ASC'], // Age (existing) + ['columnId' => self::$testColumnIds[2], 'mode' => 'ASC'], // Age (existing) ['columnId' => 999999, 'mode' => 'DESC'], // Non-existent ['columnId' => self::$testColumnIds[0], 'mode' => 'ASC'] // Name (existing) ]; @@ -292,7 +471,7 @@ public function testFindAllWithMixedExistingAndNonExistentColumns(): void { // Check that sorting works only for existing columns // Expect sorting by Age ASC, then by Name ASC (non-existent column is ignored) $nameColumnId = self::$testColumnIds[0]; - $ageColumnId = self::$testColumnIds[1]; + $ageColumnId = self::$testColumnIds[2]; $actualNameOrder = array_map(fn ($row) => $this->getCellValue($row, $nameColumnId), $rows); $actualAgeOrder = array_map(fn ($row) => $this->getCellValue($row, $ageColumnId), $rows); @@ -329,9 +508,33 @@ private function convertColumnNamesToIds(array $sortWithNames): array { return $result; } + /** + * Converts filter column names to IDs for Row2Mapper + */ + private function convertFilterColumnNamesToIds(array $filtersWithNames): array { + $columnMapping = $this->extractTestIdentMapping(self::$testDataResult['columns']); + + $result = []; + foreach ($filtersWithNames as $filterItem) { + $columnName = $filterItem['columnId']; + $operator = $filterItem['operator']; + $value = $filterItem['value']; + + if ($columnName === 'created_by') { + $result[] = ['columnId' => Column::TYPE_META_CREATED_BY, 'operator' => $operator, 'value' => $value]; + } elseif (isset($columnMapping[$columnName])) { + $result[] = ['columnId' => $columnMapping[$columnName], 'operator' => $operator, 'value' => $value]; + } else { + throw new \InvalidArgumentException("Unknown column name: $columnName"); + } + } + + return $result; + } + private function setupRealColumnMapper(int $tableId): void { $qb = $this->connection->getQueryBuilder(); - $result = $qb->select('id', 'title', 'type', 'table_id') + $result = $qb->select('*') ->from('tables_columns') ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId))) ->executeQuery(); @@ -339,11 +542,7 @@ private function setupRealColumnMapper(int $tableId): void { $columns = []; $columnTypes = []; while ($row = $result->fetch()) { - $column = new Column(); - $column->setId($row['id']); - $column->setTitle($row['title']); - $column->setType($row['type']); - $column->setTableId($row['table_id']); + $column = Column::fromRow($row); $columns[$row['id']] = $column; $columnTypes[$row['id']] = $row['type']; }