diff --git a/services/nginx/app/tests/Unit/Selfserve/SelfserveProgramRegistryContractTest.php b/services/nginx/app/tests/Unit/Selfserve/SelfserveProgramRegistryContractTest.php new file mode 100644 index 00000000..73a771a5 --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/SelfserveProgramRegistryContractTest.php @@ -0,0 +1,97 @@ + glob($dir . '/*.php') ?: [], $moduleRoutesDirs)) + ); + + expect($routeFiles)->not->toBeEmpty('Expected to find at least one route file'); + + foreach ($routeFiles as $file) { + $source = file_get_contents($file); + expect($source)->not->toBeFalse("Failed to read route file: {$file}"); + + // Check for any route that would expose a /programs-style endpoint. + // The regex matches a $this->get(...) or $this->post(...) call with a + // /programs URI segment. We use word boundaries to avoid false + // positives on /modules/self-serve/lane/relay/machine_program_picker/*. + $matches = preg_match_all( + '/\$this->(?:get|post|put|delete|patch)\s*\(\s*[\'"]\/[^\'"]*\/programs[\'"]/', + $source, + $ignored + ); + expect($matches)->toBe( + 0, + "Found a /programs endpoint in {$file}. Program names live on the wash bay hardware — " + . 'the api should not expose them. If you intentionally want to add one, update this test ' + . 'and document the new endpoint in docs/.' + ); + } +}); + +it('exposes /department/selfserve/machine-types as the api-side program-adjacent endpoint', function (): void { + $machineTypesRoute = file_get_contents(app_path('routes/departmentSelfserveMachineTypesRoute.php')); + expect($machineTypesRoute)->not->toBeFalse(); + expect($machineTypesRoute)->toContain('/department/selfserve/machine-types'); + expect($machineTypesRoute)->toContain("'list_department_selfserve_machine_types'"); + + // The route must call $response->success(...) which is the standard + // "200 OK with JSON body" envelope. The contract is: a GET to this + // endpoint returns a JSON list of machine types. + expect($machineTypesRoute)->toContain('$response->success('); + + // The route must enforce the list_* permission so unauthorized callers + // cannot enumerate machine types. + expect($machineTypesRoute)->toContain("requirePermission('list_department_selfserve_machine_types')"); +}); + +it('exposes /modules/self-serve/lane/relay/machine_program_picker/* for program picker relay control', function (): void { + $selfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); + expect($selfServeRoute)->not->toBeFalse(); + + // The program picker relay endpoints must exist. Pest's toContain does + // not accept a custom failure message, so we collect failures into a + // single assert at the end with a list of missing endpoints. + $expectedEndpoints = [ + '/modules/self-serve/lane/relay/machine_program_picker/status', + '/modules/self-serve/lane/relay/machine_program_picker/set', + '/modules/self-serve/lane/relay/machine_program_picker/enable', + ]; + + $missing = array_values(array_filter( + $expectedEndpoints, + static fn(string $endpoint): bool => !str_contains($selfServeRoute, $endpoint) + )); + + expect($missing)->toBe( + [], + 'Missing program picker relay endpoints: ' . implode(', ', $missing) + ); +});