Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ public function getProcessesVariables(array $processIds, $excludeSavedSearch, $p
return $this->mergeOnlyAvailableColumns($paginator, $savedSearch, $activeColumns);
}

return $query->paginate($perPage, ['*'], 'page', $page);
return $paginator;
}

/**
Expand Down
244 changes: 223 additions & 21 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use PDOException;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\Screen;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
Expand All @@ -18,46 +22,60 @@

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;
use RequestHelper {
setUp as requestHelperSetUp;
}

private bool $isVariablesFinderEnabled;

private ?Process $requesterProcess = null;

protected function setUp(): void
{
$this->requestHelperSetUp();
$this->configureVariableFinder();
}

/**
* Set up test environment by creating a test user and authenticating as them
*
* @return void
* Choose mock data or Variable Finder without creating processes.
* Process::factory() compiles BPMN, so it stays out of this hook.
*/
public function setupCreateUser()
private function configureVariableFinder(): void
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Clear process variables cache
$this->clearCache([1, 2, 3]);
$this->clearCache([1, 2]);

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock(true);
ProcessVariableController::useVarFinder(false);
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
ProcessVariableController::mock(false);
ProcessVariableController::useVarFinder(true);
$this->loadVariableFinderData([1, 2, 3]);

return;
}

ProcessVariableController::mock(false);
ProcessVariableController::useVarFinder(true);
}

/**
* Build the process 1/2/3 variable catalog. Call only from tests that read it.
*/
private function seedVariableFinderProcesses(array $processIds = [1, 2, 3]): void
{
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder($processIds, null);

return;
}

$this->loadVariableFinderData($processIds);
}

/**
* Test successful variables retrieval with pagination
*/
public function test_can_get_process_variables_with_pagination(): void
{
$this->seedVariableFinderProcesses();

// Make request to the endpoint
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1,2,3&page=1&per_page=15');

Expand Down Expand Up @@ -283,6 +301,8 @@ public function test_per_page_validation(): void
*/
public function test_pagination_consistency(): void
{
$this->seedVariableFinderProcesses([1]);

// Get first page
$firstPage = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&page=1&per_page=5')
->json();
Expand Down Expand Up @@ -329,6 +349,8 @@ public function test_process_ids_filtering(): void
*/
public function test_saved_search_id_filtering(): void
{
$this->seedVariableFinderProcesses([1]);

// Create a saved search with specific columns
$savedSearch = SavedSearch::factory()->create([
'meta' => [
Expand Down Expand Up @@ -544,4 +566,184 @@ public function test_saved_search_with_no_available_columns(): void
$this->assertFalse($filteredFields->contains('initiated_at'));
$this->assertFalse($filteredFields->contains('completed_at'));
}

public function test_requester_saved_search_with_empty_process_ids_and_large_data_returns_columns(): void
{
ProcessVariableController::mock(false);

$requester = User::factory()->create([
'username' => 'participant' . bin2hex(random_bytes(4)),
]);
// getProcessesAttribute() treats the PMQL literal as a process name.
$process = Process::factory()->create([
'name' => $requester->username,
]);
$this->makeRequest($requester, $process, ['requester_department' => 'engineering']);
$this->makeRequest($requester, $process, ['large_only_key' => str_repeat('x', 1048577)]);

$savedSearch = SavedSearch::factory()->create([
'type' => 'request',
'user_id' => $this->user->id,
'pmql' => '(requester = "' . $requester->username . '")',
'meta' => [
'columns' => [],
],
]);

$response = $this->apiCall(
'GET',
'/api/1.1/processes/variables?processIds=&page=1&per_page=100&savedSearchId='
. $savedSearch->id
. '&onlyAvailable='
);

$response->assertStatus(200);
$fields = collect($response->json('data'))->pluck('field');
$this->assertTrue($fields->contains('case_number'));
$this->assertTrue($fields->contains('data.requester_department'));
$this->assertTrue($fields->contains('data.large_only_key'));
}

public function test_only_available_excludes_columns_already_active_on_the_saved_search(): void
{
$savedSearch = $this->requesterSavedSearch(
['requester_department' => 'engineering'],
[
['label' => 'Case Number', 'field' => 'case_number'],
['label' => 'Department', 'field' => 'data.requester_department'],
]
);

$response = $this->getOnlyAvailable($savedSearch);

$response->assertStatus(200);
$fields = collect($response->json('data'))->pluck('field');
$this->assertFalse($fields->contains('case_number'));
$this->assertFalse($fields->contains('data.requester_department'));
$this->assertTrue($fields->contains('case_title'));
}

public function test_only_available_returns_defaults_when_discovery_hits_sort_memory(): void
{
$savedSearch = $this->requesterSavedSearch(['requester_department' => 'engineering']);

$throwSortError = true;
DB::connection()->beforeExecuting(function ($query) use (&$throwSortError) {
if (!$throwSortError || !$this->isIdSampleQuery($query)) {
return;
}

$throwSortError = false;
$previous = new PDOException(
'SQLSTATE[HY001]: Memory allocation error: 1038 Out of sort memory, consider increasing server sort buffer size'
);
$previous->errorInfo = ['HY001', 1038, 'Out of sort memory, consider increasing server sort buffer size'];

throw new QueryException(DB::connection()->getName(), $query, [], $previous);
});

try {
$response = $this->getOnlyAvailable($savedSearch);
} finally {
$throwSortError = false;
}

$response->assertStatus(200);
$fields = collect($response->json('data'))->pluck('field');
$this->assertTrue($fields->contains('case_number'));
$this->assertFalse($fields->contains('data.requester_department'));
$this->assertArrayHasKey('total', $response->json('meta'));
}

public function test_empty_process_ids_without_only_available_does_not_merge_saved_search_columns(): void
{
$savedSearch = $this->requesterSavedSearch(['requester_department' => 'engineering']);

$response = $this->apiCall(
'GET',
'/api/1.1/processes/variables?processIds=&page=1&per_page=100&savedSearchId=' . $savedSearch->id
);

$response->assertStatus(200);
$fields = collect($response->json('data'))->pluck('field');
$this->assertFalse($fields->contains('case_number'));
$this->assertFalse($fields->contains('data.requester_department'));
$this->assertSame(0, $response->json('meta.total'));
}

public function test_only_available_does_not_include_another_requesters_fields(): void
{
$savedSearch = $this->requesterSavedSearch(['requester_department' => 'engineering']);
$someoneElse = User::factory()->create();
$this->makeRequest($someoneElse, $this->requesterProcess, ['other_requester_secret' => 'nope']);

$response = $this->getOnlyAvailable($savedSearch);

$response->assertStatus(200);
$fields = collect($response->json('data'))->pluck('field');
$this->assertTrue($fields->contains('data.requester_department'));
$this->assertFalse($fields->contains('data.other_requester_secret'));
}

private function requesterSavedSearch(array $data, array $columns = []): SavedSearch
{
ProcessVariableController::mock(false);

$requester = User::factory()->create([
'username' => 'participant' . bin2hex(random_bytes(4)),
]);
$this->requesterProcess = Process::factory()->create([
'name' => $requester->username,
]);
$this->makeRequest($requester, $this->requesterProcess, $data);

return SavedSearch::factory()->create([
'type' => 'request',
'user_id' => $this->user->id,
'pmql' => '(requester = "' . $requester->username . '")',
'meta' => [
'columns' => $columns,
],
]);
}

/**
* Insert a request without model events or the factory's extra process graphs.
* The saving observer parses `data`, which is expensive for the large payload.
*/
private function makeRequest(User $user, Process $process, array $data): void
{
$versionId = $process->getLatestVersion()->id;

ProcessRequest::withoutEvents(function () use ($user, $process, $data, $versionId) {
ProcessRequest::factory()->create([
'name' => 'Request',
'status' => 'ACTIVE',
'data' => $data,
'user_id' => $user->id,
'process_id' => $process->id,
'callable_id' => 'start',
'process_collaboration_id' => null,
'process_version_id' => $versionId,
]);
});
}

private function isIdSampleQuery(string $sql): bool
{
$normalized = strtolower($sql);

return str_contains($normalized, 'select `process_requests`.`id` from')
&& str_contains($normalized, 'order by `process_requests`.`id` desc');
}

private function getOnlyAvailable(SavedSearch $savedSearch)
{
return $this->apiCall(
'GET',
'/api/1.1/processes/variables?processIds=&page=1&per_page=100&savedSearchId='
. $savedSearch->id
. '&onlyAvailable='
);
}
}
Loading