Skip to content

Commit fdd28de

Browse files
test: add tests for process listing, including pagination, sorting, and response structure validation
1 parent a550247 commit fdd28de

1 file changed

Lines changed: 329 additions & 0 deletions

File tree

‎tests/Feature/Api/ProcessTest.php‎

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
use Illuminate\Foundation\Testing\WithFaker;
88
use Illuminate\Http\UploadedFile;
99
use Illuminate\Support\Facades\Auth;
10+
use Illuminate\Support\Facades\DB;
1011
use Illuminate\Support\Facades\Hash;
1112
use ProcessMaker\Models\Bookmark;
1213
use ProcessMaker\Models\Group;
1314
use ProcessMaker\Models\GroupMember;
1415
use ProcessMaker\Models\Permission;
1516
use ProcessMaker\Models\Process;
1617
use ProcessMaker\Models\ProcessCategory;
18+
use ProcessMaker\Models\ProcessLaunchpad;
19+
use ProcessMaker\Models\ProcessNotificationSetting;
1720
use ProcessMaker\Models\ProcessRequest;
1821
use ProcessMaker\Models\User;
1922
use ProcessMaker\Providers\WorkflowServiceProvider as PM;
@@ -629,6 +632,332 @@ public function testPagination()
629632
$response->assertJsonCount((2 + $initialRows) % 5, 'data');
630633
}
631634

635+
public function testProcessListingResponseSupportsDesignerTables()
636+
{
637+
$manager = User::factory()->create();
638+
$process = Process::factory()->create([
639+
'name' => 'Timer Process Listing Contract',
640+
'description' => 'Used by process row actions',
641+
'bpmn' => file_get_contents(__DIR__ . '/processes/ProcessStartTimerEvent.bpmn'),
642+
'pause_timer_start' => true,
643+
'properties' => ['manager_id' => [$manager->id]],
644+
'warnings' => [['message' => 'Test warning']],
645+
]);
646+
647+
$response = $this->apiCall('GET', route('api.processes.index', [
648+
'filter' => $process->name,
649+
'include' => 'categories,category,user',
650+
]));
651+
652+
$response->assertOk();
653+
$row = collect($response->json('data'))->firstWhere('id', $process->id);
654+
655+
$this->assertNotNull($row);
656+
$this->assertSame($process->description, $row['description']);
657+
$this->assertSame([$manager->id], $row['manager_id']);
658+
$this->assertTrue($row['has_timer_start_events']);
659+
$this->assertTrue((bool) $row['pause_timer_start']);
660+
$this->assertArrayHasKey('warnings', $row);
661+
$this->assertArrayHasKey('case_retention_tier_adjustment_notice', $row);
662+
$this->assertArrayHasKey('user', $row);
663+
$this->assertArrayHasKey('categories', $row);
664+
$this->assertArrayHasKey('category', $row);
665+
$this->assertArrayHasKey('notifications', $row);
666+
$this->assertArrayHasKey('task_notifications', $row);
667+
$this->assertArrayNotHasKey('events', $row);
668+
$this->assertArrayNotHasKey('startEvents', $row);
669+
$this->assertArrayNotHasKey('projects', $row);
670+
$this->assertArrayNotHasKey('bpmn', $row);
671+
$this->assertArrayNotHasKey('svg', $row);
672+
}
673+
674+
public function testProcessListingQueriesAreBoundedByPageSize()
675+
{
676+
$user = User::factory()->create();
677+
$category = ProcessCategory::factory()->create();
678+
$attributes = [
679+
'user_id' => $user->id,
680+
'process_category_id' => $category->id,
681+
];
682+
Process::factory()->count(20)->create($attributes);
683+
684+
$route = route('api.processes.index', [
685+
'page' => 1,
686+
'per_page' => 15,
687+
'include' => 'categories,category,user',
688+
]);
689+
690+
// Warm schema and application caches before comparing query counts.
691+
$this->apiCall('GET', $route);
692+
$initialQueries = $this->captureListingQueries($route);
693+
694+
Process::factory()->count(100)->create($attributes);
695+
$queriesWithMoreProcesses = $this->captureListingQueries($route);
696+
697+
$this->assertCount(15, $this->apiCall('GET', $route)->json('data'));
698+
$this->assertCount(count($initialQueries), $queriesWithMoreProcesses);
699+
700+
$listingQuery = collect($queriesWithMoreProcesses)->first(function ($query) {
701+
$sql = strtolower($query['query']);
702+
703+
return str_contains($sql, 'from `processes`') && str_contains($sql, 'limit 15');
704+
});
705+
706+
$this->assertNotNull($listingQuery, 'The process listing query must apply the requested SQL limit.');
707+
$this->assertStringNotContainsString('`processes`.*', $listingQuery['query']);
708+
$this->assertStringNotContainsString('`processes`.`bpmn`', $listingQuery['query']);
709+
$this->assertStringNotContainsString('`processes`.`svg`', $listingQuery['query']);
710+
711+
$projectQueries = collect($queriesWithMoreProcesses)->filter(
712+
fn ($query) => str_contains($query['query'], 'from `project_assets`')
713+
);
714+
$notificationQueries = collect($queriesWithMoreProcesses)->filter(
715+
fn ($query) => str_contains($query['query'], 'from `process_notification_settings`')
716+
);
717+
$versionQueries = collect($queriesWithMoreProcesses)->filter(function ($query) {
718+
preg_match('/\bfrom\s+`?([^`\s]+)`?/i', $query['query'], $matches);
719+
720+
return ($matches[1] ?? null) === 'process_versions';
721+
});
722+
723+
$this->assertCount(0, $projectQueries);
724+
$this->assertCount(1, $notificationQueries);
725+
$this->assertCount(0, $versionQueries);
726+
}
727+
728+
public function testProcessListingSortsByDesignerRelatedFields()
729+
{
730+
$firstUser = User::factory()->create(['username' => 'aaaa-process-owner']);
731+
$lastUser = User::factory()->create(['username' => 'zzzz-process-owner']);
732+
$firstCategory = ProcessCategory::factory()->create(['name' => 'AAAA Process Category']);
733+
$lastCategory = ProcessCategory::factory()->create(['name' => 'ZZZZ Process Category']);
734+
$lastProcess = Process::factory()->create([
735+
'name' => 'Related Process Sort Last',
736+
'user_id' => $lastUser->id,
737+
'process_category_id' => $lastCategory->id,
738+
]);
739+
$firstProcess = Process::factory()->create([
740+
'name' => 'Related Process Sort First',
741+
'user_id' => $firstUser->id,
742+
'process_category_id' => $firstCategory->id,
743+
]);
744+
745+
$parameters = ['filter' => 'Related Process Sort', 'order_direction' => 'asc'];
746+
$response = $this->apiCall('GET', route('api.processes.index', [
747+
...$parameters,
748+
'order_by' => 'user.username',
749+
]));
750+
$response->assertJsonPath('data.0.id', $firstProcess->id);
751+
752+
$response = $this->apiCall('GET', route('api.processes.index', [
753+
...$parameters,
754+
'order_by' => 'category.name',
755+
]));
756+
$response->assertJsonPath('data.0.id', $firstProcess->id);
757+
$response->assertJsonFragment(['id' => $lastProcess->id]);
758+
}
759+
760+
public function testProcessListingPaginatesPmqlResults()
761+
{
762+
Process::factory()->count(3)->create(['name' => 'PMQL Pagination Match']);
763+
Process::factory()->create(['name' => 'PMQL Pagination Nonmatch']);
764+
765+
$response = $this->apiCall('GET', route('api.processes.index', [
766+
'pmql' => 'name = "PMQL Pagination Match"',
767+
'per_page' => 2,
768+
]));
769+
770+
$response->assertOk();
771+
$response->assertJsonCount(2, 'data');
772+
$response->assertJsonPath('meta.total', 3);
773+
$response->assertJsonPath('meta.last_page', 2);
774+
}
775+
776+
public function testProcessListingLimitsIncludesAndEagerLoadsToCurrentPage()
777+
{
778+
$bpmn = trim(Process::getProcessTemplate('SingleTask.bpmn'));
779+
$processes = collect(range(1, 5))->map(function ($number) use ($bpmn) {
780+
$process = Process::factory()->create([
781+
'name' => sprintf('Advanced Page Boundary %02d', $number),
782+
'bpmn' => $bpmn,
783+
]);
784+
ProcessNotificationSetting::factory()->create([
785+
'process_id' => $process->id,
786+
'notifiable_type' => 'requester',
787+
'notification_type' => 'started',
788+
]);
789+
790+
return $process;
791+
});
792+
$parameters = [
793+
'filter' => 'Advanced Page Boundary',
794+
'order_by' => 'name',
795+
'order_direction' => 'asc',
796+
'page' => 2,
797+
'per_page' => 2,
798+
'include' => 'category,user',
799+
];
800+
$routeWithoutEvents = route('api.processes.index', $parameters);
801+
$routeWithEvents = route('api.processes.index', [
802+
...$parameters,
803+
'include' => 'category,user,events',
804+
]);
805+
806+
$this->apiCall('GET', $routeWithoutEvents);
807+
$this->apiCall('GET', $routeWithEvents);
808+
$queriesWithoutEvents = $this->captureListingQueries($routeWithoutEvents);
809+
$queriesWithEvents = $this->captureListingQueries($routeWithEvents);
810+
$response = $this->apiCall('GET', $routeWithEvents);
811+
$expectedIds = $processes->slice(2, 2)->pluck('id')->values()->all();
812+
813+
$response->assertOk();
814+
$this->assertSame($expectedIds, collect($response->json('data'))->pluck('id')->all());
815+
$this->assertCount(count($queriesWithoutEvents), $queriesWithEvents);
816+
foreach ($response->json('data') as $row) {
817+
$this->assertCount(1, $row['events']);
818+
$this->assertArrayNotHasKey('bpmn', $row);
819+
$this->assertArrayNotHasKey('svg', $row);
820+
}
821+
822+
$notificationQuery = collect($queriesWithEvents)->first(
823+
fn ($query) => str_contains($query['query'], 'from `process_notification_settings`')
824+
);
825+
826+
$this->assertNotNull($notificationQuery);
827+
$this->assertSame(
828+
1,
829+
preg_match('/`process_id` in \(([^)]+)\)/', $notificationQuery['query'], $matches)
830+
);
831+
$eagerLoadedProcessIds = array_map('intval', preg_split('/,\s*/', $matches[1]));
832+
$this->assertEqualsCanonicalizing(
833+
$expectedIds,
834+
$eagerLoadedProcessIds
835+
);
836+
}
837+
838+
public function testProcessListingSerializesBatchedNotificationSettings()
839+
{
840+
$process = Process::factory()->create(['name' => 'Advanced Notification Payload']);
841+
ProcessNotificationSetting::factory()->create([
842+
'process_id' => $process->id,
843+
'notifiable_type' => 'requester',
844+
'notification_type' => 'started',
845+
]);
846+
ProcessNotificationSetting::factory()->create([
847+
'process_id' => $process->id,
848+
'notifiable_type' => 'manager',
849+
'notification_type' => 'error',
850+
]);
851+
ProcessNotificationSetting::factory()->create([
852+
'process_id' => $process->id,
853+
'element_id' => 'AdvancedTask',
854+
'notifiable_type' => 'assignee',
855+
'notification_type' => 'assigned',
856+
]);
857+
ProcessNotificationSetting::factory()->create([
858+
'process_id' => $process->id,
859+
'element_id' => 'AdvancedTask',
860+
'notifiable_type' => 'manager',
861+
'notification_type' => 'due',
862+
]);
863+
$route = route('api.processes.index', ['filter' => $process->name]);
864+
865+
$queries = $this->captureListingQueries($route);
866+
$response = $this->apiCall('GET', $route);
867+
868+
$response->assertOk();
869+
$response->assertJsonPath('data.0.notifications.requester.started', true);
870+
$response->assertJsonPath('data.0.notifications.requester.completed', false);
871+
$response->assertJsonPath('data.0.notifications.manager.error', true);
872+
$response->assertJsonPath('data.0.task_notifications.AdvancedTask.assignee.assigned', true);
873+
$response->assertJsonPath('data.0.task_notifications.AdvancedTask.assignee.due', false);
874+
$response->assertJsonPath('data.0.task_notifications.AdvancedTask.manager.due', true);
875+
$this->assertArrayNotHasKey('notification_settings', $response->json('data.0'));
876+
$this->assertCount(1, collect($queries)->filter(
877+
fn ($query) => str_contains($query['query'], 'from `process_notification_settings`')
878+
));
879+
}
880+
881+
public function testProcessListingOnlyRunsOptionalLookupsWhenRequested()
882+
{
883+
$process = Process::factory()->create(['name' => 'Advanced Optional Lookups']);
884+
$bookmark = Bookmark::factory()->create([
885+
'process_id' => $process->id,
886+
'user_id' => $this->user->id,
887+
]);
888+
$launchpad = ProcessLaunchpad::factory()->create([
889+
'process_id' => $process->id,
890+
'user_id' => $this->user->id,
891+
]);
892+
$defaultRoute = route('api.processes.index', ['filter' => $process->name]);
893+
894+
$defaultQueries = collect($this->captureListingQueries($defaultRoute));
895+
$defaultResponse = $this->apiCall('GET', $defaultRoute);
896+
897+
$defaultResponse->assertJsonPath('data.0.bookmark_id', 0);
898+
$defaultResponse->assertJsonPath('data.0.launchpad', null);
899+
$this->assertFalse($defaultQueries->contains(
900+
fn ($query) => str_contains($query['query'], 'from `user_process_bookmarks`')
901+
));
902+
$this->assertFalse($defaultQueries->contains(
903+
fn ($query) => str_contains($query['query'], 'from `process_launchpad`')
904+
));
905+
906+
$enabledRoute = route('api.processes.index', [
907+
'filter' => $process->name,
908+
'bookmark' => true,
909+
'launchpad' => true,
910+
]);
911+
$enabledQueries = collect($this->captureListingQueries($enabledRoute));
912+
$enabledResponse = $this->apiCall('GET', $enabledRoute);
913+
914+
$enabledResponse->assertJsonPath('data.0.bookmark_id', $bookmark->id);
915+
$enabledResponse->assertJsonPath('data.0.launchpad.id', $launchpad->id);
916+
$this->assertCount(1, $enabledQueries->filter(
917+
fn ($query) => str_contains($query['query'], 'from `user_process_bookmarks`')
918+
));
919+
$this->assertCount(1, $enabledQueries->filter(
920+
fn ($query) => str_contains($query['query'], 'from `process_launchpad`')
921+
));
922+
}
923+
924+
public function testProcessListingDoesNotEagerLoadRelationshipsForEmptyPage()
925+
{
926+
Process::factory()->count(3)->create(['name' => 'Advanced Empty Page']);
927+
$route = route('api.processes.index', [
928+
'filter' => 'Advanced Empty Page',
929+
'page' => 3,
930+
'per_page' => 2,
931+
'include' => 'category,user,events',
932+
]);
933+
934+
$queries = collect($this->captureListingQueries($route));
935+
$response = $this->apiCall('GET', $route);
936+
937+
$response->assertOk();
938+
$response->assertJsonCount(0, 'data');
939+
$response->assertJsonPath('meta.total', 3);
940+
$response->assertJsonPath('meta.current_page', 3);
941+
$response->assertJsonPath('meta.last_page', 2);
942+
$this->assertFalse($queries->contains(
943+
fn ($query) => str_contains($query['query'], 'from `process_notification_settings`')
944+
));
945+
}
946+
947+
private function captureListingQueries(string $route): array
948+
{
949+
$connection = DB::connection('processmaker');
950+
$connection->flushQueryLog();
951+
$connection->enableQueryLog();
952+
953+
$this->apiCall('GET', $route)->assertOk();
954+
$queries = $connection->getQueryLog();
955+
956+
$connection->disableQueryLog();
957+
958+
return $queries;
959+
}
960+
632961
/**
633962
* Test the creation of processes
634963
*/

0 commit comments

Comments
 (0)