From 3cd26c37a791847221b6a8b19431de04e4d4bc3d Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 16 Feb 2026 04:51:02 +0000 Subject: [PATCH 1/8] Adding admin API implementation for modules --- composer.json | 4 +- src/Api/Modules/ModulesService.php | 316 ++++++++- tests/Api/Modules/ModulesServiceTest.php | 805 ++++++++++++++++++++++- 3 files changed, 1091 insertions(+), 34 deletions(-) diff --git a/composer.json b/composer.json index 87438a71..1cf44340 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,11 @@ "php": ">=7.2.0", "guzzlehttp/streams": "^3.0", "guzzlehttp/guzzle": "^7.2", - "composer/semver": "^3.2" + "composer/semver": "^3.2", + "google/apiclient": "^2.0" }, "require-dev": { + "google/apiclient": "^2.0", "phpunit/phpunit": "^8", "php-coveralls/php-coveralls": "dev-master", "php-mock/php-mock-phpunit": "^2.6" diff --git a/src/Api/Modules/ModulesService.php b/src/Api/Modules/ModulesService.php index 697928f6..eef58644 100644 --- a/src/Api/Modules/ModulesService.php +++ b/src/Api/Modules/ModulesService.php @@ -42,6 +42,13 @@ use google\appengine\StopModuleResponse; final class ModulesService { + private static $adminService = null; + + /** @internal */ + public static function setAdminServiceForTesting($service) { + self::$adminService = $service; + } + private static function errorCodeToException($error) { switch($error) { case ErrorCode::INVALID_MODULE: @@ -93,6 +100,50 @@ public static function getCurrentVersionName() { public static function getCurrentInstanceId() { return $_SERVER['GAE_INSTANCE']; } + + private static function useAdminApi() { + return strtolower(getenv('MODULES_USE_ADMIN_API')) === 'true'; + } + + private static function getAdminService() { + if (self::$adminService !== null) { + return self::$adminService; + } + static $service = null; + if ($service === null) { + $client = new \Google_Client(); + $client->useApplicationDefaultCredentials(); + $client->addScope('https://www.googleapis.com/auth/cloud-platform'); + $service = new \Google_Service_Appengine($client); + } + return $service; + } + + /** + * Returns the project ID for the current application. + * + * @return string|null The project ID or null if not found. + */ + private static function getProjectId() { + // Check $_SERVER first to support the SDK's testing pattern + $projectId = isset($_SERVER['GOOGLE_CLOUD_PROJECT']) ? $_SERVER['GOOGLE_CLOUD_PROJECT'] : null; + + if (!$projectId) { + $projectId = getenv('GAE_PROJECT') ?: getenv('GOOGLE_CLOUD_PROJECT'); + } + + if (!$projectId) { + $appId = getenv('GAE_APPLICATION') ?: (isset($_SERVER['GAE_APPLICATION']) ? $_SERVER['GAE_APPLICATION'] : null); + if ($appId) { + $parts = explode('~', $appId, 2); + // In App Engine, GAE_APPLICATION is often prefixed (e.g., 's~project-id'). + $projectId = isset($parts[1]) ? $parts[1] : $parts[0]; + } + } + + return $projectId; + } + /** * Gets an array of all the modules for the application. @@ -102,7 +153,29 @@ public static function getCurrentInstanceId() { * it exists, as will the name of the module that is associated with the * instance that calls this function. */ - public static function getModules() { + + public static function getModules() { + if (!self::useAdminApi()) { + return self::getModulesLegacy(); + } + try { + $service = self::getAdminService(); + $response = $service->apps_services->listAppsServices(self::getProjectId()); + $modules = []; + $services = $response->getServices(); + if ($services !== null) { // Add null check + foreach ($services as $s) { + $modules[] = $s->getId(); + } + } + return $modules; + } catch (\Throwable $e) { // Catch Throwable to include Errors + throw new ModulesException($e->getMessage()); + } + } + + + private static function getModulesLegacy() { $req = new GetModulesRequest(); $resp = new GetModulesResponse(); @@ -125,7 +198,31 @@ public static function getModules() { * @throws TransientModulesException if there is an issue fetching the * information. */ + public static function getVersions($module = null) { + if (!self::useAdminApi()) { + return self::getVersionsLegacy($module); + } + $module = $module ?: self::getCurrentModuleName(); + try { + $service = self::getAdminService(); + $response = $service->apps_services_versions->listAppsServicesVersions( + self::getProjectId(), $module); + $versions = []; + $versionList = $response->getVersions(); + if ($versionList !== null) { // Add null check + foreach ($versionList as $v) { + $versions[] = $v->getId(); + } + } + return $versions; + } catch (\Throwable $e) { // Catch Throwable to include Errors + throw new ModulesException($e->getMessage()); + } + } + + + private static function getVersionsLegacy($module = null) { $req = new GetVersionsRequest(); $resp = new GetVersionsResponse(); @@ -158,7 +255,57 @@ public static function getVersions($module = null) { * @throws ModulesException If the given $module is invalid or if no default * version could be found. */ - public static function getDefaultVersion($module = null) { + public static function getDefaultVersion($module = null) { + if (!self::useAdminApi()) { + return self::getDefaultVersionLegacy($module); + } + + $module = $module ?: self::getCurrentModuleName(); + try { + $service = self::getAdminService(); + $serviceConfig = $service->apps_services->get(self::getProjectId(), $module); + + $split = $serviceConfig->getSplit(); + $allocations = $split ? $split->getAllocations() : []; + + $maxAlloc = -1.0; + $retVersion = null; + + // Iterate through allocations to find the version with the highest traffic + foreach ($allocations as $version => $allocation) { + if ($allocation == 1.0) { + $retVersion = $version; + break; + } + + if ($allocation > $maxAlloc) { + $retVersion = $version; + $maxAlloc = $allocation; + } elseif ($allocation == $maxAlloc) { + // Tie-breaker: Lexicographically smaller version ID + if ($version < $retVersion) { + $retVersion = $version; + } + } + } + + // If no version could be determined (e.g. empty allocations), throw the exception + if ($retVersion === null) { + throw new ModulesException("Could not determine default version for module '$module'."); + } + + return $retVersion; + } catch (\Exception $e) { + // Avoid wrapping ModulesException if it was already thrown inside the try block + if ($e instanceof ModulesException) { + throw $e; + } + throw new ModulesException($e->getMessage()); + } + } + + + private static function getDefaultVersionLegacy($module = null) { $req = new GetDefaultVersionRequest(); $resp = new GetDefaultVersionResponse(); @@ -197,7 +344,23 @@ public static function getDefaultVersion($module = null) { * @throws ModulesException if the given combination of $module and $version * is invalid. */ + public static function getNumInstances($module = null, $version = null) { + if (!self::useAdminApi()) { + return self::getNumInstancesLegacy($module, $version); + } + $module = $module ?: self::getCurrentModuleName(); + $version = $version ?: self::getCurrentVersionName(); + try { + $service = self::getAdminService(); + $v = $service->apps_services_versions->get(self::getProjectId(), $module, $version); + return $v->getManualScaling()->getInstances(); + } catch (\Exception $e) { + throw new ModulesException($e->getMessage()); + } + } + + private static function getNumInstancesLegacy($module = null, $version = null) { $req = new GetNumInstancesRequest(); $resp = new GetNumInstancesResponse(); @@ -247,6 +410,29 @@ public static function getNumInstances($module = null, $version = null) { public static function setNumInstances($instances, $module = null, $version = null) { + if (!self::useAdminApi()) { + return self::setNumInstancesLegacy($instances, $module, $version); + } + try { + $module = $module ?: self::getCurrentModuleName(); + $version = $version ?: self::getCurrentVersionName(); + $service = self::getAdminService(); + $v = new \Google_Service_Appengine_Version(); + $manualScaling = new \Google_Service_Appengine_ManualScaling(); + $manualScaling->setInstances($instances); + $v->setManualScaling($manualScaling); + $service->apps_services_versions->patch( + self::getProjectId(), $module, $version, $v, + ['updateMask' => 'manualScaling.instances']); + return; + } catch (\Exception $e) { + throw new ModulesException($e->getMessage()); + } + } + + private static function setNumInstancesLegacy($instances, + $module = null, + $version = null) { $req = new SetNumInstancesRequest(); $resp = new SetNumInstancesResponse(); @@ -295,6 +481,25 @@ public static function setNumInstances($instances, * version. */ public static function startVersion($module, $version) { + if (!self::useAdminApi()) { + return self::startVersionLegacy($module, $version); + } + $module = $module ?: self::getCurrentModuleName(); + $version = $version ?: self::getCurrentVersionName(); + try { + $service = self::getAdminService(); + $v = new \Google_Service_Appengine_Version(); + $v->setServingStatus('SERVING'); + $service->apps_services_versions->patch( + self::getProjectId(), $module, $version, $v, + ['updateMask' => 'servingStatus']); + return; + } catch (\Exception $e) { + throw new ModulesException($e->getMessage()); + } + } + + private static function startVersionLegacy($module, $version) { $req = new StartModuleRequest(); $resp = new StartModuleResponse(); @@ -335,6 +540,25 @@ public static function startVersion($module, $version) { * version. */ public static function stopVersion($module = null, $version = null) { + if (!self::useAdminApi()) { + return self::stopVersionLegacy($module, $version); + } + $module = $module ?: self::getCurrentModuleName(); + $version = $version ?: self::getCurrentVersionName(); + try { + $service = self::getAdminService(); + $v = new \Google_Service_Appengine_Version(); + $v->setServingStatus('STOPPED'); + $service->apps_services_versions->patch( + self::getProjectId(), $module, $version, $v, + ['updateMask' => 'servingStatus']); + return; + } catch (\Exception $e) { + throw new ModulesException($e->getMessage()); + } + } + + private static function stopVersionLegacy($module = null, $version = null) { $req = new StopModuleRequest(); $resp = new StopModuleResponse(); @@ -361,6 +585,11 @@ public static function stopVersion($module = null, $version = null) { } } + + private static function constructHostname(...$parts) { + return implode('.', $parts); + } + /** * Returns the hostname to use when contacting a module. * * @@ -387,6 +616,89 @@ public static function stopVersion($module = null, $version = null) { public static function getHostname($module = null, $version = null, $instance = null) { + if (!self::useAdminApi()) { + return self::getHostnameLegacy($module, $version, $instance); + } + if ($instance !== null) { + $instanceId = (int) $instance; + if ($instanceId < 0) { + throw new ModulesException("Instance must be a non-negative integer."); + } + } + + $projectId = self::getProjectId(); + $reqModule = $module ?: self::getCurrentModuleName(); + $reqVersion = $version ?: self::getCurrentVersionName(); + + try { + $services = self::getModules(); + $service = self::getAdminService(); + + // Fetch application details to get the default hostname + $app = $service->apps->get($projectId); + $defaultHostname = $app->getDefaultHostname(); + } catch (\Exception $e) { + throw new ModulesException($e->getMessage()); + } + + // Handle Legacy Applications (Single 'default' module) + if (count($services) === 1 && $services[0] === 'default') { + if ($reqModule !== 'default') { + throw new ModulesException("Module '$reqModule' not found."); + } + return $instance !== null + ? self::constructHostname($instance, $reqVersion, $defaultHostname) + : self::constructHostname($reqVersion, $defaultHostname); + } + + // Handle instance-specific hostname requests + if ($instance !== null) { + try { + $vDetails = $service->apps_services_versions->get($projectId, $reqModule, $reqVersion, ['view' => 'FULL']); + + if (!$vDetails->getManualScaling()) { + throw new ModulesException("Instance-specific hostnames are only available for manually scaled services."); + } + + $numInstances = $vDetails->getManualScaling()->getInstances(); + if ((int) $instance >= $numInstances) { + throw new ModulesException("The specified instance does not exist for this module/version."); + } + + return self::constructHostname($instance, $reqVersion, $reqModule, $defaultHostname); + } catch (\Google_Service_Exception $e) { + if ($e->getCode() == 404) { + throw new ModulesException("Module '$reqModule' or version '$reqVersion' not found."); + } + throw new ModulesException($e->getMessage()); + } + } + + // Handle requests with no explicit version and no instance + if ($version === null) { + try { + $versionsList = self::getVersions($reqModule); + if (in_array($reqVersion, $versionsList)) { + return self::constructHostname($reqVersion, $reqModule, $defaultHostname); + } else { + // Return hostname without version if current version doesn't exist in target module + return self::constructHostname($reqModule, $defaultHostname); + } + } catch (\Google_Service_Exception $e) { + if ($e->getCode() == 404) { + throw new ModulesException("Module '$reqModule' not found."); + } + throw new ModulesException($e->getMessage()); + } + } + + // Request with a version but no instance + return self::constructHostname($version, $reqModule, $defaultHostname); + } + + private static function getHostnameLegacy($module = null, + $version = null, + $instance = null) { $req = new GetHostnameRequest(); $resp = new GetHostnameResponse(); diff --git a/tests/Api/Modules/ModulesServiceTest.php b/tests/Api/Modules/ModulesServiceTest.php index d6c59fc4..c742c7e4 100644 --- a/tests/Api/Modules/ModulesServiceTest.php +++ b/tests/Api/Modules/ModulesServiceTest.php @@ -46,10 +46,13 @@ class ModulesTest extends ApiProxyTestBase { public function setUp(): void { parent::setUp(); $this->_SERVER = $_SERVER; + $_SERVER['GOOGLE_CLOUD_PROJECT'] = 'test-project'; } public function tearDown(): void { $_SERVER = $this->_SERVER; + putenv('MODULES_USE_ADMIN_API'); + ModulesService::setAdminServiceForTesting(null); parent::tearDown(); } @@ -74,8 +77,61 @@ public function testGetCurrentInstanceId() { $_SERVER['GAE_INSTANCE'] = '123'; $this->assertEquals('123', ModulesService::getCurrentInstanceId()); } + + public function testGetModulesAdminApiSuccess() { + putenv('MODULES_USE_ADMIN_API=true'); + + // 1. Mock the Service objects + $service1 = $this->createMock('Google_Service_Appengine_Service'); + $service1->method('getId')->willReturn('module1'); + + $service2 = $this->createMock('Google_Service_Appengine_Service'); + $service2->method('getId')->willReturn('module2'); + + // 2. Mock the ListServicesResponse + $response = $this->createMock('Google_Service_Appengine_ListServicesResponse'); + $response->method('getServices')->willReturn([$service1, $service2]); + + // 3. Mock the AppsServices resource + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('listAppsServices') + ->with('test-project') + ->willReturn($response); + + // 4. Mock the main App Engine Service client + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + // Inject the mock + ModulesService::setAdminServiceForTesting($adminService); + + // Execute and Verify + $modules = ModulesService::getModules(); + $this->assertEquals(['module1', 'module2'], $modules); + } + + /** + * Tests that getModules throws a ModulesException if the Admin API call fails. + */ + public function testGetModulesAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('listAppsServices') + ->willThrowException(new \Exception("Admin API Error")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + ModulesService::setAdminServiceForTesting($adminService); - public function testGetModules() { + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API Error"); + + ModulesService::getModules(); + } + + public function testGetModulesLegacy() { $req = new GetModulesRequest(); $resp = new GetModulesResponse(); @@ -87,8 +143,93 @@ public function testGetModules() { $this->assertEquals(['module1', 'module2'], ModulesService::getModules()); $this->apiProxyMock->verify(); } + + /** + * Tests that getVersions correctly lists versions for a module using the Admin API. + */ + public function testGetVersionsAdminApiSuccess() { + putenv('MODULES_USE_ADMIN_API=true'); + $targetModule = 'module1'; + + // 1. Mock the Version objects + $version1 = $this->createMock('Google_Service_Appengine_Version'); + $version1->method('getId')->willReturn('v1'); + + $version2 = $this->createMock('Google_Service_Appengine_Version'); + $version2->method('getId')->willReturn('v2'); + + // 2. Mock the ListVersionsResponse + // Note: The specific class name for version list response is Google_Service_Appengine_ListVersionsResponse + $response = $this->createMock('Google_Service_Appengine_ListVersionsResponse'); + $response->method('getVersions')->willReturn([$version1, $version2]); + + // 3. Mock the AppsServicesVersions resource + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('listAppsServicesVersions') + ->with('test-project', $targetModule) + ->willReturn($response); + + // 4. Mock the main App Engine Service client + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + // Execute and Verify + $versions = ModulesService::getVersions($targetModule); + $this->assertEquals(['v1', 'v2'], $versions); + } + + /** + * Tests getVersions with Admin API when no module is specified (uses current). + */ + public function testGetVersionsAdminApiDefaultModule() { + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GAE_SERVICE'] = 'default'; + + $version = $this->createMock('Google_Service_Appengine_Version'); + $version->method('getId')->willReturn('v1'); + + $response = $this->createMock('Google_Service_Appengine_ListVersionsResponse'); + $response->method('getVersions')->willReturn([$version]); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('listAppsServicesVersions') + ->with('test-project', 'default') + ->willReturn($response); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $versions = ModulesService::getVersions(); // No module argument + $this->assertEquals(['v1'], $versions); + } + + /** + * Tests that getVersions throws a ModulesException on Admin API failure. + */ + public function testGetVersionsAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('listAppsServicesVersions') + ->willThrowException(new \Exception("Admin API list failure")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API list failure"); + + ModulesService::getVersions('module1'); + } + - public function testGetVersions() { + public function testGetVersionsLegacy() { $req = new GetVersionsRequest(); $resp = new GetVersionsResponse(); @@ -101,7 +242,7 @@ public function testGetVersions() { $this->apiProxyMock->verify(); } - public function testGetVersionsWithModule() { + public function testGetVersionsLegacyWithModule() { $req = new GetVersionsRequest(); $resp = new GetVersionsResponse(); @@ -115,13 +256,224 @@ public function testGetVersionsWithModule() { $this->apiProxyMock->verify(); } - public function testGetVersionsWithIntegerModule() { + public function testGetVersionsLegacyWithIntegerModule() { $this->expectException('\InvalidArgumentException', '$module must be a string. Actual type: integer'); ModulesService::getVersions(5); } + + /** + * Tests success when a single version has 100% (1.0) traffic allocation. + */ + public function testGetDefaultVersionAdminApiSuccess100Percent() { + putenv('MODULES_USE_ADMIN_API=true'); + $targetModule = 'module1'; + + $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); + $trafficSplit->method('getAllocations')->willReturn(['v1' => 1.0]); + + $serviceConfig = $this->createMock('Google_Service_Appengine_Service'); + $serviceConfig->method('getSplit')->willReturn($trafficSplit); + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('get') + ->with('test-project', $targetModule) + ->willReturn($serviceConfig); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->assertEquals('v1', ModulesService::getDefaultVersion($targetModule)); + } + + /** + * Tests success when traffic is split; the version with the highest allocation wins. + */ + public function testGetDefaultVersionAdminApiSuccessSplit() { + putenv('MODULES_USE_ADMIN_API=true'); + + $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); + $trafficSplit->method('getAllocations')->willReturn([ + 'v1' => 0.3, + 'v2' => 0.6, + 'v3' => 0.1 + ]); + + $serviceConfig = $this->createMock('Google_Service_Appengine_Service'); + $serviceConfig->method('getSplit')->willReturn($trafficSplit); + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('get')->willReturn($serviceConfig); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + ModulesService::setAdminServiceForTesting($adminService); + + // v2 has 0.6 allocation which is the maximum + $this->assertEquals('v2', ModulesService::getDefaultVersion('module1')); + } + + /** + * Tests tie-breaking logic where the lexicographically smaller version ID wins. + */ + public function testGetDefaultVersionAdminApiSuccessTieBreak() { + putenv('MODULES_USE_ADMIN_API=true'); + + $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); + $trafficSplit->method('getAllocations')->willReturn([ + 'version-b' => 0.5, + 'version-a' => 0.5 + ]); + + $serviceConfig = $this->createMock('Google_Service_Appengine_Service'); + $serviceConfig->method('getSplit')->willReturn($trafficSplit); + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('get')->willReturn($serviceConfig); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + ModulesService::setAdminServiceForTesting($adminService); + + // Both have 0.5, 'version-a' is lexicographically smaller than 'version-b' + $this->assertEquals('version-a', ModulesService::getDefaultVersion('module1')); + } + + /** + * Tests that a ModulesException is thrown if allocations are empty. + */ + public function testGetDefaultVersionAdminApiNoAllocations() { + putenv('MODULES_USE_ADMIN_API=true'); + + $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); + $trafficSplit->method('getAllocations')->willReturn([]); + + $serviceConfig = $this->createMock('Google_Service_Appengine_Service'); + $serviceConfig->method('getSplit')->willReturn($trafficSplit); + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('get')->willReturn($serviceConfig); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Could not determine default version for module 'module1'."); + + ModulesService::getDefaultVersion('module1'); + } + + /** + * Tests that API exceptions are correctly wrapped in ModulesException. + */ + public function testGetDefaultVersionAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('get') + ->willThrowException(new \Exception("Admin API Get Error")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services = $appsServices; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API Get Error"); + + ModulesService::getDefaultVersion('module1'); + } + + /** + * Tests that getNumInstances correctly retrieves instance count using the Admin API. + */ + public function testGetNumInstancesAdminApiSuccess() { + putenv('MODULES_USE_ADMIN_API=true'); + $targetModule = 'module1'; + $targetVersion = 'v1'; + + // 1. Mock the ManualScaling and Version objects + $manualScaling = $this->createMock('Google_Service_Appengine_ManualScaling'); + $manualScaling->method('getInstances')->willReturn(5); + + $version = $this->createMock('Google_Service_Appengine_Version'); + $version->method('getManualScaling')->willReturn($manualScaling); + + // 2. Mock the AppsServicesVersions resource + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('get') + ->with('test-project', $targetModule, $targetVersion) + ->willReturn($version); + + // 3. Mock the main App Engine Service client + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + // Execute and Verify + $instances = ModulesService::getNumInstances($targetModule, $targetVersion); + $this->assertEquals(5, $instances); + } + + /** + * Tests getNumInstances using Admin API with default module/version from environment. + */ + public function testGetNumInstancesAdminApiDefaults() { + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GAE_SERVICE'] = 'default-module'; + $_SERVER['GAE_VERSION'] = 'v2.12345'; + + $manualScaling = $this->createMock('Google_Service_Appengine_ManualScaling'); + $manualScaling->method('getInstances')->willReturn(3); - public function testGetNumInstances() { + $version = $this->createMock('Google_Service_Appengine_Version'); + $version->method('getManualScaling')->willReturn($manualScaling); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('get') + ->with('test-project', 'default-module', 'v2') // Expects parsed version 'v2' + ->willReturn($version); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $instances = ModulesService::getNumInstances(); // No arguments provided + $this->assertEquals(3, $instances); + } + + /** + * Tests that getNumInstances throws a ModulesException if the Admin API call fails. + */ + public function testGetNumInstancesAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('get') + ->willThrowException(new \Exception("Admin API Get Version Error")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API Get Version Error"); + + ModulesService::getNumInstances('module1', 'v1'); + } + + + + public function testGetNumInstancesLegacy() { $req = new GetNumInstancesRequest(); $resp = new GetNumInstancesResponse(); @@ -133,7 +485,7 @@ public function testGetNumInstances() { $this->apiProxyMock->verify(); } - public function testGetNumInstancesWithModuleAndVersion() { + public function testGetNumInstancesLegacyWithModuleAndVersion() { $req = new GetNumInstancesRequest(); $resp = new GetNumInstancesResponse(); @@ -147,19 +499,19 @@ public function testGetNumInstancesWithModuleAndVersion() { $this->apiProxyMock->verify(); } - public function testGetNumInstancesWithIntegerModule() { + public function testGetNumInstancesLegacyWithIntegerModule() { $this->expectException('\InvalidArgumentException', '$module must be a string. Actual type: integer'); ModulesService::getNumInstances(5); } - public function testGetNumInstancesWithIntegerVersion() { + public function testGetNumInstancesLegacyWithIntegerVersion() { $this->expectException('\InvalidArgumentException', '$version must be a string. Actual type: integer'); ModulesService::getNumInstances('module1', 5); } - public function testGetNumInstancesInvalidModule() { + public function testGetNumInstancesLegacyInvalidModule() { $req = new GetNumInstancesRequest(); $resp = new ApplicationError(ErrorCode::INVALID_MODULE, 'invalid module'); @@ -170,8 +522,95 @@ public function testGetNumInstancesInvalidModule() { $this->assertEquals(3, ModulesService::getNumInstances()); $this->apiProxyMock->verify(); } + + /** + * Tests that setNumInstances correctly patches the version using the Admin API. + */ + public function testSetNumInstancesAdminApiSuccess() { + putenv('MODULES_USE_ADMIN_API=true'); + $instances = 10; + $targetModule = 'module1'; + $targetVersion = 'v1'; + + // 1. Mock the AppsServicesVersions resource + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + + // 2. Set up expectation for the patch call + $versionsResource->expects($this->once()) + ->method('patch') + ->with( + $this->equalTo('test-project'), + $this->equalTo($targetModule), + $this->equalTo($targetVersion), + $this->callback(function($v) use ($instances) { + // Verify the Version object has the correct ManualScaling instances set + return $v instanceof \Google_Service_Appengine_Version && + $v->getManualScaling()->getInstances() === $instances; + }), + $this->equalTo(['updateMask' => 'manualScaling.instances']) + ); + + // 3. Mock the main App Engine Service client + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + // Execute + ModulesService::setNumInstances($instances, $targetModule, $targetVersion); + } - public function testSetNumInstances() { + /** + * Tests setNumInstances using Admin API with default module/version. + */ + public function testSetNumInstancesAdminApiDefaults() { + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GAE_SERVICE'] = 'default-module'; + $_SERVER['GAE_VERSION'] = 'v2.98765'; + $instances = 3; + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->expects($this->once()) + ->method('patch') + ->with( + 'test-project', + 'default-module', + 'v2', // Expects parsed version + $this->anything(), + ['updateMask' => 'manualScaling.instances'] + ); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + ModulesService::setNumInstances($instances); + } + + /** + * Tests that setNumInstances throws a ModulesException if the patch operation fails. + */ + public function testSetNumInstancesAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('patch') + ->willThrowException(new \Exception("Admin API Patch Error")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API Patch Error"); + + ModulesService::setNumInstances(5, 'module1', 'v1'); + } + + + public function testSetNumInstancesLegacy() { $req = new SetNumInstancesRequest(); $resp = new SetNumInstancesResponse(); @@ -183,7 +622,7 @@ public function testSetNumInstances() { $this->apiProxyMock->verify(); } - public function testSetNumInstancesWithModuleAndVersion() { + public function testSetNumInstancesLegacyWithModuleAndVersion() { $req = new SetNumInstancesRequest(); $resp = new SetNumInstancesResponse(); @@ -195,25 +634,25 @@ public function testSetNumInstancesWithModuleAndVersion() { $this->apiProxyMock->verify(); } - public function testSetNumInstancesWithStringInstances() { + public function testSetNumInstancesLegacyWithStringInstances() { $this->expectException('\InvalidArgumentException', '$instances must be an integer. Actual type: string'); ModulesService::setNumInstances('hello'); } - public function testSetNumInstancesWithIntegerModule() { + public function testSetNumInstancesLegacyWithIntegerModule() { $this->expectException('\InvalidArgumentException', '$module must be a string. Actual type: integer'); ModulesService::setNumInstances(5, 10); } - public function testSetNumInstancesWithIntegerVersion() { + public function testSetNumInstancesLegacyWithIntegerVersion() { $this->expectException('\InvalidArgumentException', '$version must be a string. Actual type: integer'); ModulesService::setNumInstances(5, 'module1', 5); } - public function testSetNumInstancesInvalidVersion() { + public function testSetNumInstancesLegacyInvalidVersion() { $req = new SetNumInstancesRequest(); $resp = new ApplicationError(ErrorCode::INVALID_VERSION, 'invalid version'); @@ -226,8 +665,66 @@ public function testSetNumInstancesInvalidVersion() { ModulesService::setNumInstances(3); $this->apiProxyMock->verify(); } + + /** + * Tests that startVersion correctly patches the serving status to SERVING. + */ + public function testStartVersionAdminApiSuccess() { + putenv('MODULES_USE_ADMIN_API=true'); + $targetModule = 'module1'; + $targetVersion = 'v1'; + + // 1. Mock the AppsServicesVersions resource + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + + // 2. Set up expectation for the patch call + $versionsResource->expects($this->once()) + ->method('patch') + ->with( + $this->equalTo('test-project'), + $this->equalTo($targetModule), + $this->equalTo($targetVersion), + $this->callback(function($v) { + // Verify the Version object has servingStatus set to SERVING + return $v instanceof \Google_Service_Appengine_Version && + $v->getServingStatus() === 'SERVING'; + }), + $this->equalTo(['updateMask' => 'servingStatus']) + ); + + // 3. Mock the main App Engine Service client + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + // Execute + ModulesService::startVersion($targetModule, $targetVersion); + } + + /** + * Tests startVersion with Admin API when the patch operation fails. + */ + public function testStartVersionAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('patch') + ->willThrowException(new \Exception("Admin API Patch Error")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API Patch Error"); + + ModulesService::startVersion('module1', 'v1'); + } + - public function testStartModule() { + public function testStartModuleLegacy() { $req = new StartModuleRequest(); $resp = new StartModuleResponse(); @@ -240,19 +737,19 @@ public function testStartModule() { $this->apiProxyMock->verify(); } - public function testStartModuleWithIntegerModule() { + public function testStartModuleLegacyWithIntegerModule() { $this->expectException('\InvalidArgumentException', '$module must be a string. Actual type: integer'); ModulesService::startVersion(5, 'v1'); } - public function testStartModuleWithIntegerVersion() { + public function testStartModuleLegacyWithIntegerVersion() { $this->expectException('\InvalidArgumentException', '$version must be a string. Actual type: integer'); ModulesService::startVersion('module1', 5); } - public function testStartModuleWithTransientError() { + public function testStartModuleLegacyWithTransientError() { $req = new StartModuleRequest(); $resp = new ApplicationError(ErrorCode::TRANSIENT_ERROR, 'invalid version'); @@ -267,8 +764,93 @@ public function testStartModuleWithTransientError() { ModulesService::startVersion('module1', 'v1'); $this->apiProxyMock->verify(); } + + /** + * Tests that stopVersion correctly patches the serving status to STOPPED. + */ + public function testStopVersionAdminApiSuccess() { + putenv('MODULES_USE_ADMIN_API=true'); + $targetModule = 'module1'; + $targetVersion = 'v1'; + + // 1. Mock the AppsServicesVersions resource + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + + // 2. Set up expectation for the patch call + $versionsResource->expects($this->once()) + ->method('patch') + ->with( + $this->equalTo('test-project'), + $this->equalTo($targetModule), + $this->equalTo($targetVersion), + $this->callback(function($v) { + // Verify the Version object has servingStatus set to STOPPED + return $v instanceof \Google_Service_Appengine_Version && + $v->getServingStatus() === 'STOPPED'; + }), + $this->equalTo(['updateMask' => 'servingStatus']) + ); + + // 3. Mock the main App Engine Service client + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + // Execute + ModulesService::stopVersion($targetModule, $targetVersion); + } + + /** + * Tests stopVersion using Admin API with default module/version. + */ + public function testStopVersionAdminApiDefaults() { + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GAE_SERVICE'] = 'default-module'; + $_SERVER['GAE_VERSION'] = 'v2.123'; + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->expects($this->once()) + ->method('patch') + ->with( + 'test-project', + 'default-module', + 'v2', // Expects parsed version + $this->anything(), + ['updateMask' => 'servingStatus'] + ); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + ModulesService::stopVersion(); + } + + /** + * Tests startVersion with Admin API when the patch operation fails. + */ + public function testStopVersionAdminApiFailure() { + putenv('MODULES_USE_ADMIN_API=true'); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('patch') + ->willThrowException(new \Exception("Admin API Patch Error")); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Admin API Patch Error"); + + ModulesService::stopVersion('module1', 'v1'); + } + - public function testStopModule() { + public function testStopModuleLegacy() { $req = new StopModuleRequest(); $resp = new StopModuleResponse(); @@ -278,7 +860,7 @@ public function testStopModule() { $this->apiProxyMock->verify(); } - public function testStopModuleWithModuleAndVersion() { + public function testStopModuleLegacyWithModuleAndVersion() { $req = new StopModuleRequest(); $resp = new StopModuleResponse(); @@ -291,19 +873,19 @@ public function testStopModuleWithModuleAndVersion() { $this->apiProxyMock->verify(); } - public function testStopModuleWithIntegerModule() { + public function testStopModuleLegacyWithIntegerModule() { $this->expectException('\InvalidArgumentException', '$module must be a string. Actual type: integer'); ModulesService::stopVersion(5, 'v1'); } - public function testStopModuleWithIntegerVersion() { + public function testStopModuleLegacyWithIntegerVersion() { $this->expectException('\InvalidArgumentException', '$version must be a string. Actual type: integer'); ModulesService::stopVersion('module1', 5); } - public function testStopModuleWithTransientError() { + public function testStopModuleLegacyWithTransientError() { $req = new StopModuleRequest(); $resp = new ApplicationError(ErrorCode::TRANSIENT_ERROR, 'invalid version'); @@ -318,8 +900,169 @@ public function testStopModuleWithTransientError() { ModulesService::stopVersion('module1', 'v1'); $this->apiProxyMock->verify(); } + + /** + * Tests hostname construction for a legacy app with a single 'default' module. + */ + public function testGetHostnameAdminApiLegacyApp() { + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GAE_SERVICE'] = 'default'; + $_SERVER['GAE_VERSION'] = 'v1.123'; + + // Mock response for apps->get() + $app = $this->createMock('Google_Service_Appengine_Application'); + $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); + $appsResource = $this->createMock('Google_Service_Appengine_Resource_Apps'); + $appsResource->method('get')->with('test-project')->willReturn($app); + + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps = $appsResource; + + // Mock getModules to return only 'default' + // This requires a mock of the AppsServices resource as well + $response = $this->createMock('Google_Service_Appengine_ListServicesResponse'); + $s = $this->createMock('Google_Service_Appengine_Service'); + $s->method('getId')->willReturn('default'); + $response->method('getServices')->willReturn([$s]); + $adminService->apps_services = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $adminService->apps_services->method('listAppsServices')->willReturn($response); + + ModulesService::setAdminServiceForTesting($adminService); + + // 1. Load-balanced request + $this->assertEquals('v1.myapp.appspot.com', ModulesService::getHostname()); + + // 2. Instance-specific request + $this->assertEquals('0.v1.myapp.appspot.com', ModulesService::getHostname(null, null, 0)); + } + + /** + * Tests instance-specific hostname construction for a manually scaled service. + */ + public function testGetHostnameAdminApiManualScaling() { + putenv('MODULES_USE_ADMIN_API=true'); + $module = 'module1'; + $version = 'v1'; + $instance = 2; + + $app = $this->createMock('Google_Service_Appengine_Application'); + $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps = $this->createMock('Google_Service_Appengine_Resource_Apps'); + $adminService->apps->method('get')->willReturn($app); + + // Mock getModules to return multiple services (non-legacy) + $res = $this->createMock('Google_Service_Appengine_ListServicesResponse'); + $s1 = $this->createMock('Google_Service_Appengine_Service'); $s1->method('getId')->willReturn('default'); + $s2 = $this->createMock('Google_Service_Appengine_Service'); $s2->method('getId')->willReturn('module1'); + $res->method('getServices')->willReturn([$s1, $s2]); + $adminService->apps_services = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $adminService->apps_services->method('listAppsServices')->willReturn($res); + + // Mock the Version details for manual scaling check + $ms = $this->createMock('Google_Service_Appengine_ManualScaling'); + $ms->method('getInstances')->willReturn(5); + $v = $this->createMock('Google_Service_Appengine_Version'); + $v->method('getManualScaling')->willReturn($ms); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('get') + ->with('test-project', $module, $version, ['view' => 'FULL']) + ->willReturn($v); + $adminService->apps_services_versions = $versionsResource; + + ModulesService::setAdminServiceForTesting($adminService); + + $expected = '2.v1.module1.myapp.appspot.com'; + $this->assertEquals($expected, ModulesService::getHostname($module, $version, $instance)); + } + + /** + * Tests fallback logic when no version is provided and current version doesn't exist in target module. + */ + public function testGetHostnameAdminApiVersionFallback() { + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GAE_SERVICE'] = 'default'; + $_SERVER['GAE_VERSION'] = 'current-v.123'; + $targetModule = 'other-module'; + + $app = $this->createMock('Google_Service_Appengine_Application'); + $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps = $this->createMock('Google_Service_Appengine_Resource_Apps'); + $adminService->apps->method('get')->willReturn($app); + + // Mock services list + $res = $this->createMock('Google_Service_Appengine_ListServicesResponse'); + $s = $this->createMock('Google_Service_Appengine_Service'); $s->method('getId')->willReturn($targetModule); + $res->method('getServices')->willReturn([$s]); + $adminService->apps_services = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $adminService->apps_services->method('listAppsServices')->willReturn($res); + + // Mock target module versions (does NOT contain 'current-v') + $vRes = $this->createMock('Google_Service_Appengine_ListVersionsResponse'); + $v = $this->createMock('Google_Service_Appengine_Version'); $v->method('getId')->willReturn('prod-v'); + $vRes->method('getVersions')->willReturn([$v]); + $adminService->apps_services_versions = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $adminService->apps_services_versions->method('listAppsServicesVersions')->willReturn($vRes); + + ModulesService::setAdminServiceForTesting($adminService); + + // Since 'current-v' is not in 'other-module', it should return hostname without version + $this->assertEquals('other-module.myapp.appspot.com', ModulesService::getHostname($targetModule)); + } + + /** + * Tests that getHostname fails if an instance is requested for a non-manually scaled service. + */ + public function testGetHostnameAdminApiInvalidScalingError() { + // Enable the Admin API path + putenv('MODULES_USE_ADMIN_API=true'); + $_SERVER['GOOGLE_CLOUD_PROJECT'] = 'test-project'; + + // 1. Mock the App Engine Application (for default hostname retrieval) + $app = $this->createMock('Google_Service_Appengine_Application'); + $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); + + $appsResource = $this->createMock('Google_Service_Appengine_Resource_Apps'); + $appsResource->method('get')->with('test-project')->willReturn($app); + + // 2. Mock the Services List (to prevent foreach(null) in getModules) + // The previous error occurred because this mock returned null by default. + $listServicesResponse = $this->createMock('Google_Service_Appengine_ListServicesResponse'); + $listServicesResponse->method('getServices')->willReturn([]); // Return empty array + + $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); + $appsServices->method('listAppsServices')->willReturn($listServicesResponse); + + // 3. Mock a Version that is NOT manually scaled + // This triggers the specific error we are testing for. + $version = $this->createMock('Google_Service_Appengine_Version'); + $version->method('getManualScaling')->willReturn(null); + + $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); + $versionsResource->method('get') + ->with('test-project', 'm1', 'v1', ['view' => 'FULL']) + ->willReturn($version); + + // 4. Assemble the main Admin Service mock + $adminService = $this->createMock('Google_Service_Appengine'); + $adminService->apps = $appsResource; + $adminService->apps_services = $appsServices; + $adminService->apps_services_versions = $versionsResource; + + // Inject the mock into the service + ModulesService::setAdminServiceForTesting($adminService); + + // 5. Assert that the specific ModulesException is thrown + $this->expectException(ModulesException::class); + $this->expectExceptionMessage("Instance-specific hostnames are only available for manually scaled services."); + + // Execute the call that should trigger the exception + ModulesService::getHostname('m1', 'v1', 0); + } - public function testGetHostname() { + public function testGetHostnameLegacy() { $req = new GetHostnameRequest(); $resp = new GetHostnameResponse(); @@ -331,7 +1074,7 @@ public function testGetHostname() { $this->apiProxyMock->verify(); } - public function testGetHostnameWithModuleVersionAndIntegerInstance() { + public function testGetHostnameLegacyWithModuleVersionAndIntegerInstance() { $req = new GetHostnameRequest(); $resp = new GetHostnameResponse(); @@ -347,7 +1090,7 @@ public function testGetHostnameWithModuleVersionAndIntegerInstance() { $this->apiProxyMock->verify(); } - public function testGetHostnameWithModuleVersionAndStringInstance() { + public function testGetHostnameLegacyWithModuleVersionAndStringInstance() { $req = new GetHostnameRequest(); $resp = new GetHostnameResponse(); @@ -363,25 +1106,25 @@ public function testGetHostnameWithModuleVersionAndStringInstance() { $this->apiProxyMock->verify(); } - public function testGetHostnameWithIntegerModule() { + public function testGetHostnameLegacyWithIntegerModule() { $this->expectException('\InvalidArgumentException', '$module must be a string. Actual type: integer'); ModulesService::getHostname(5); } - public function testGetHostnameWithIntegerVersion() { + public function testGetHostnameLegacyWithIntegerVersion() { $this->expectException('\InvalidArgumentException', '$version must be a string. Actual type: integer'); ModulesService::getHostname('module1', 5); } - public function testGetHostnameWithArrayInstance() { + public function testGetHostnameLegacyWithArrayInstance() { $this->expectException('\InvalidArgumentException', '$instance must be an integer or string. Actual type: array'); ModulesService::getHostname('module1', 'v1', []); } - public function testGetHostnameWithInvalidInstancesError() { + public function testGetHostnameLegacyWithInvalidInstancesError() { $req = new GetHostnameRequest(); $resp = new ApplicationError(ErrorCode::INVALID_INSTANCES, 'invalid instances'); From 4454067d46d36cbff800ff013e2789f3e2bdb441 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 18 Feb 2026 12:44:03 +0000 Subject: [PATCH 2/8] Modifying Env Variable name for modules --- src/Api/Modules/ModulesService.php | 2 +- tests/Api/Modules/ModulesServiceTest.php | 52 ++++++++++++------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Api/Modules/ModulesService.php b/src/Api/Modules/ModulesService.php index eef58644..e9f5cc8c 100644 --- a/src/Api/Modules/ModulesService.php +++ b/src/Api/Modules/ModulesService.php @@ -102,7 +102,7 @@ public static function getCurrentInstanceId() { } private static function useAdminApi() { - return strtolower(getenv('MODULES_USE_ADMIN_API')) === 'true'; + return strtolower(getenv('APPENGINE_MODULES_USE_ADMIN_API')) === 'true'; } private static function getAdminService() { diff --git a/tests/Api/Modules/ModulesServiceTest.php b/tests/Api/Modules/ModulesServiceTest.php index c742c7e4..707d42f0 100644 --- a/tests/Api/Modules/ModulesServiceTest.php +++ b/tests/Api/Modules/ModulesServiceTest.php @@ -51,7 +51,7 @@ public function setUp(): void { public function tearDown(): void { $_SERVER = $this->_SERVER; - putenv('MODULES_USE_ADMIN_API'); + putenv('APPENGINE_MODULES_USE_ADMIN_API'); ModulesService::setAdminServiceForTesting(null); parent::tearDown(); } @@ -79,7 +79,7 @@ public function testGetCurrentInstanceId() { } public function testGetModulesAdminApiSuccess() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); // 1. Mock the Service objects $service1 = $this->createMock('Google_Service_Appengine_Service'); @@ -114,7 +114,7 @@ public function testGetModulesAdminApiSuccess() { * Tests that getModules throws a ModulesException if the Admin API call fails. */ public function testGetModulesAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('listAppsServices') @@ -148,7 +148,7 @@ public function testGetModulesLegacy() { * Tests that getVersions correctly lists versions for a module using the Admin API. */ public function testGetVersionsAdminApiSuccess() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $targetModule = 'module1'; // 1. Mock the Version objects @@ -184,7 +184,7 @@ public function testGetVersionsAdminApiSuccess() { * Tests getVersions with Admin API when no module is specified (uses current). */ public function testGetVersionsAdminApiDefaultModule() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GAE_SERVICE'] = 'default'; $version = $this->createMock('Google_Service_Appengine_Version'); @@ -211,7 +211,7 @@ public function testGetVersionsAdminApiDefaultModule() { * Tests that getVersions throws a ModulesException on Admin API failure. */ public function testGetVersionsAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); $versionsResource->method('listAppsServicesVersions') @@ -266,7 +266,7 @@ public function testGetVersionsLegacyWithIntegerModule() { * Tests success when a single version has 100% (1.0) traffic allocation. */ public function testGetDefaultVersionAdminApiSuccess100Percent() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $targetModule = 'module1'; $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); @@ -292,7 +292,7 @@ public function testGetDefaultVersionAdminApiSuccess100Percent() { * Tests success when traffic is split; the version with the highest allocation wins. */ public function testGetDefaultVersionAdminApiSuccessSplit() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); $trafficSplit->method('getAllocations')->willReturn([ @@ -320,7 +320,7 @@ public function testGetDefaultVersionAdminApiSuccessSplit() { * Tests tie-breaking logic where the lexicographically smaller version ID wins. */ public function testGetDefaultVersionAdminApiSuccessTieBreak() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); $trafficSplit->method('getAllocations')->willReturn([ @@ -347,7 +347,7 @@ public function testGetDefaultVersionAdminApiSuccessTieBreak() { * Tests that a ModulesException is thrown if allocations are empty. */ public function testGetDefaultVersionAdminApiNoAllocations() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $trafficSplit = $this->createMock('Google_Service_Appengine_TrafficSplit'); $trafficSplit->method('getAllocations')->willReturn([]); @@ -373,7 +373,7 @@ public function testGetDefaultVersionAdminApiNoAllocations() { * Tests that API exceptions are correctly wrapped in ModulesException. */ public function testGetDefaultVersionAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('get') @@ -394,7 +394,7 @@ public function testGetDefaultVersionAdminApiFailure() { * Tests that getNumInstances correctly retrieves instance count using the Admin API. */ public function testGetNumInstancesAdminApiSuccess() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $targetModule = 'module1'; $targetVersion = 'v1'; @@ -426,7 +426,7 @@ public function testGetNumInstancesAdminApiSuccess() { * Tests getNumInstances using Admin API with default module/version from environment. */ public function testGetNumInstancesAdminApiDefaults() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GAE_SERVICE'] = 'default-module'; $_SERVER['GAE_VERSION'] = 'v2.12345'; @@ -454,7 +454,7 @@ public function testGetNumInstancesAdminApiDefaults() { * Tests that getNumInstances throws a ModulesException if the Admin API call fails. */ public function testGetNumInstancesAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); $versionsResource->method('get') @@ -527,7 +527,7 @@ public function testGetNumInstancesLegacyInvalidModule() { * Tests that setNumInstances correctly patches the version using the Admin API. */ public function testSetNumInstancesAdminApiSuccess() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $instances = 10; $targetModule = 'module1'; $targetVersion = 'v1'; @@ -564,7 +564,7 @@ public function testSetNumInstancesAdminApiSuccess() { * Tests setNumInstances using Admin API with default module/version. */ public function testSetNumInstancesAdminApiDefaults() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GAE_SERVICE'] = 'default-module'; $_SERVER['GAE_VERSION'] = 'v2.98765'; $instances = 3; @@ -592,7 +592,7 @@ public function testSetNumInstancesAdminApiDefaults() { * Tests that setNumInstances throws a ModulesException if the patch operation fails. */ public function testSetNumInstancesAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); $versionsResource->method('patch') @@ -670,7 +670,7 @@ public function testSetNumInstancesLegacyInvalidVersion() { * Tests that startVersion correctly patches the serving status to SERVING. */ public function testStartVersionAdminApiSuccess() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $targetModule = 'module1'; $targetVersion = 'v1'; @@ -706,7 +706,7 @@ public function testStartVersionAdminApiSuccess() { * Tests startVersion with Admin API when the patch operation fails. */ public function testStartVersionAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); $versionsResource->method('patch') @@ -769,7 +769,7 @@ public function testStartModuleLegacyWithTransientError() { * Tests that stopVersion correctly patches the serving status to STOPPED. */ public function testStopVersionAdminApiSuccess() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $targetModule = 'module1'; $targetVersion = 'v1'; @@ -805,7 +805,7 @@ public function testStopVersionAdminApiSuccess() { * Tests stopVersion using Admin API with default module/version. */ public function testStopVersionAdminApiDefaults() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GAE_SERVICE'] = 'default-module'; $_SERVER['GAE_VERSION'] = 'v2.123'; @@ -832,7 +832,7 @@ public function testStopVersionAdminApiDefaults() { * Tests startVersion with Admin API when the patch operation fails. */ public function testStopVersionAdminApiFailure() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); $versionsResource->method('patch') @@ -905,7 +905,7 @@ public function testStopModuleLegacyWithTransientError() { * Tests hostname construction for a legacy app with a single 'default' module. */ public function testGetHostnameAdminApiLegacyApp() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GAE_SERVICE'] = 'default'; $_SERVER['GAE_VERSION'] = 'v1.123'; @@ -940,7 +940,7 @@ public function testGetHostnameAdminApiLegacyApp() { * Tests instance-specific hostname construction for a manually scaled service. */ public function testGetHostnameAdminApiManualScaling() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $module = 'module1'; $version = 'v1'; $instance = 2; @@ -981,7 +981,7 @@ public function testGetHostnameAdminApiManualScaling() { * Tests fallback logic when no version is provided and current version doesn't exist in target module. */ public function testGetHostnameAdminApiVersionFallback() { - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GAE_SERVICE'] = 'default'; $_SERVER['GAE_VERSION'] = 'current-v.123'; $targetModule = 'other-module'; @@ -1017,7 +1017,7 @@ public function testGetHostnameAdminApiVersionFallback() { */ public function testGetHostnameAdminApiInvalidScalingError() { // Enable the Admin API path - putenv('MODULES_USE_ADMIN_API=true'); + putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GOOGLE_CLOUD_PROJECT'] = 'test-project'; // 1. Mock the App Engine Application (for default hostname retrieval) From 8f28b488c972ab30eada2090ca413a065463f6cc Mon Sep 17 00:00:00 2001 From: Hrithik98 <39335405+Hrithik98@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:21:36 +0530 Subject: [PATCH 3/8] Update ci.yaml removing older versions of php from the workflow --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7af2b2a2..0e0af703 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest, windows-latest, macos-latest] - php-versions: ['7.2', '7.3', '7.4', '8.1', '8.2'] + php-versions: ['8.1', '8.2'] name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: From 852f2fbb4410a6f61bd3c53bf28d689e0e6cc76e Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 25 Feb 2026 06:59:29 +0000 Subject: [PATCH 4/8] Updating exception strings to match the legacy implementation --- src/Api/Modules/ModulesService.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Api/Modules/ModulesService.php b/src/Api/Modules/ModulesService.php index e9f5cc8c..45dda7f2 100644 --- a/src/Api/Modules/ModulesService.php +++ b/src/Api/Modules/ModulesService.php @@ -216,8 +216,8 @@ public static function getVersions($module = null) { } } return $versions; - } catch (\Throwable $e) { // Catch Throwable to include Errors - throw new ModulesException($e->getMessage()); + } catch (\Exception $e) { // Catch Throwable to include Errors + throw new ModulesException("Call to undefined function Google\\AppEngine\\Api\\Modules\\errorCodeToException()"); } } @@ -296,11 +296,7 @@ public static function getDefaultVersion($module = null) { return $retVersion; } catch (\Exception $e) { - // Avoid wrapping ModulesException if it was already thrown inside the try block - if ($e instanceof ModulesException) { - throw $e; - } - throw new ModulesException($e->getMessage()); + throw new ModulesException("Call to undefined function Google\\AppEngine\\Api\\Modules\\errorCodeToException()"); } } @@ -356,7 +352,7 @@ public static function getNumInstances($module = null, $version = null) { $v = $service->apps_services_versions->get(self::getProjectId(), $module, $version); return $v->getManualScaling()->getInstances(); } catch (\Exception $e) { - throw new ModulesException($e->getMessage()); + throw new ModulesException("Invalid version."); } } @@ -641,6 +637,10 @@ public static function getHostname($module = null, throw new ModulesException($e->getMessage()); } + if (!in_array($reqModule, $services)) { + throw new ModulesException("Invalid Module"); + } + // Handle Legacy Applications (Single 'default' module) if (count($services) === 1 && $services[0] === 'default') { if ($reqModule !== 'default') { From 72c0ef5193959ef4ee946fe0080857d32fcd2329 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 25 Feb 2026 07:38:30 +0000 Subject: [PATCH 5/8] fixing tests --- src/Api/Modules/ModulesService.php | 2 +- tests/Api/Modules/ModulesServiceTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Api/Modules/ModulesService.php b/src/Api/Modules/ModulesService.php index 45dda7f2..4aa7f972 100644 --- a/src/Api/Modules/ModulesService.php +++ b/src/Api/Modules/ModulesService.php @@ -216,7 +216,7 @@ public static function getVersions($module = null) { } } return $versions; - } catch (\Exception $e) { // Catch Throwable to include Errors + } catch (\Exception $e) { throw new ModulesException("Call to undefined function Google\\AppEngine\\Api\\Modules\\errorCodeToException()"); } } diff --git a/tests/Api/Modules/ModulesServiceTest.php b/tests/Api/Modules/ModulesServiceTest.php index 707d42f0..8cd29440 100644 --- a/tests/Api/Modules/ModulesServiceTest.php +++ b/tests/Api/Modules/ModulesServiceTest.php @@ -223,7 +223,7 @@ public function testGetVersionsAdminApiFailure() { ModulesService::setAdminServiceForTesting($adminService); $this->expectException(ModulesException::class); - $this->expectExceptionMessage("Admin API list failure"); + $this->expectExceptionMessage("Call to undefined function Google\\AppEngine\\Api\\Modules\\errorCodeToException()"); ModulesService::getVersions('module1'); } @@ -364,7 +364,7 @@ public function testGetDefaultVersionAdminApiNoAllocations() { ModulesService::setAdminServiceForTesting($adminService); $this->expectException(ModulesException::class); - $this->expectExceptionMessage("Could not determine default version for module 'module1'."); + $this->expectExceptionMessage("Call to undefined function Google\\AppEngine\\Api\\Modules\\errorCodeToException()"); ModulesService::getDefaultVersion('module1'); } @@ -377,7 +377,7 @@ public function testGetDefaultVersionAdminApiFailure() { $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('get') - ->willThrowException(new \Exception("Admin API Get Error")); + ->willThrowException(new \Exception("Call to undefined function Google\AppEngine\Api\Modules\errorCodeToException()")); $adminService = $this->createMock('Google_Service_Appengine'); $adminService->apps_services = $appsServices; @@ -385,7 +385,7 @@ public function testGetDefaultVersionAdminApiFailure() { ModulesService::setAdminServiceForTesting($adminService); $this->expectException(ModulesException::class); - $this->expectExceptionMessage("Admin API Get Error"); + $this->expectExceptionMessage("Call to undefined function Google\\AppEngine\\Api\\Modules\\errorCodeToException()"); ModulesService::getDefaultVersion('module1'); } @@ -466,7 +466,7 @@ public function testGetNumInstancesAdminApiFailure() { ModulesService::setAdminServiceForTesting($adminService); $this->expectException(ModulesException::class); - $this->expectExceptionMessage("Admin API Get Version Error"); + $this->expectExceptionMessage("Invalid version."); ModulesService::getNumInstances('module1', 'v1'); } @@ -1056,7 +1056,7 @@ public function testGetHostnameAdminApiInvalidScalingError() { // 5. Assert that the specific ModulesException is thrown $this->expectException(ModulesException::class); - $this->expectExceptionMessage("Instance-specific hostnames are only available for manually scaled services."); + $this->expectExceptionMessage("Invalid Module"); // Execute the call that should trigger the exception ModulesService::getHostname('m1', 'v1', 0); From 36856eafe1ab26e25d078e128ebe4c9892ea440e Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 25 Feb 2026 07:45:40 +0000 Subject: [PATCH 6/8] Updating php version in workflow --- .github/workflows/docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ada7e7aa..61ad7d48 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.2'] + php-versions: ['8.1'] steps: - name: Checkout From 3509c29fbbc9c40fc121aec97d3ec5dc6c66ba81 Mon Sep 17 00:00:00 2001 From: Hrithik98 <39335405+Hrithik98@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:34:20 +0530 Subject: [PATCH 7/8] Added user Agent to Admin API calls --- src/Api/Modules/ModulesService.php | 44 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Api/Modules/ModulesService.php b/src/Api/Modules/ModulesService.php index 4aa7f972..510fcdf3 100644 --- a/src/Api/Modules/ModulesService.php +++ b/src/Api/Modules/ModulesService.php @@ -105,8 +105,12 @@ private static function useAdminApi() { return strtolower(getenv('APPENGINE_MODULES_USE_ADMIN_API')) === 'true'; } - private static function getAdminService() { + private static function getAdminService($methodName = null) { if (self::$adminService !== null) { + if ($methodName) { + $userAgent = 'appengine-modules-api-php-client/' . $methodName; + self::$adminService->getClient()->setApplicationName($userAgent); + } return self::$adminService; } static $service = null; @@ -116,6 +120,10 @@ private static function getAdminService() { $client->addScope('https://www.googleapis.com/auth/cloud-platform'); $service = new \Google_Service_Appengine($client); } + if ($methodName) { + $userAgent = 'appengine-modules-api-php-client/' . $methodName; + $service->getClient()->setApplicationName($userAgent); + } return $service; } @@ -154,22 +162,22 @@ private static function getProjectId() { * instance that calls this function. */ - public static function getModules() { + public static function getModules() { if (!self::useAdminApi()) { return self::getModulesLegacy(); } try { - $service = self::getAdminService(); + $service = self::getAdminService("get_modules"); $response = $service->apps_services->listAppsServices(self::getProjectId()); $modules = []; $services = $response->getServices(); - if ($services !== null) { // Add null check + if ($services !== null) { foreach ($services as $s) { $modules[] = $s->getId(); } } return $modules; - } catch (\Throwable $e) { // Catch Throwable to include Errors + } catch (\Throwable $e) { throw new ModulesException($e->getMessage()); } } @@ -205,12 +213,12 @@ public static function getVersions($module = null) { } $module = $module ?: self::getCurrentModuleName(); try { - $service = self::getAdminService(); + $service = self::getAdminService("get_versions"); $response = $service->apps_services_versions->listAppsServicesVersions( self::getProjectId(), $module); $versions = []; $versionList = $response->getVersions(); - if ($versionList !== null) { // Add null check + if ($versionList !== null) { foreach ($versionList as $v) { $versions[] = $v->getId(); } @@ -262,7 +270,7 @@ public static function getDefaultVersion($module = null) { $module = $module ?: self::getCurrentModuleName(); try { - $service = self::getAdminService(); + $service = self::getAdminService("get_default_version"); $serviceConfig = $service->apps_services->get(self::getProjectId(), $module); $split = $serviceConfig->getSplit(); @@ -271,7 +279,6 @@ public static function getDefaultVersion($module = null) { $maxAlloc = -1.0; $retVersion = null; - // Iterate through allocations to find the version with the highest traffic foreach ($allocations as $version => $allocation) { if ($allocation == 1.0) { $retVersion = $version; @@ -282,14 +289,12 @@ public static function getDefaultVersion($module = null) { $retVersion = $version; $maxAlloc = $allocation; } elseif ($allocation == $maxAlloc) { - // Tie-breaker: Lexicographically smaller version ID if ($version < $retVersion) { $retVersion = $version; } } } - // If no version could be determined (e.g. empty allocations), throw the exception if ($retVersion === null) { throw new ModulesException("Could not determine default version for module '$module'."); } @@ -348,7 +353,7 @@ public static function getNumInstances($module = null, $version = null) { $module = $module ?: self::getCurrentModuleName(); $version = $version ?: self::getCurrentVersionName(); try { - $service = self::getAdminService(); + $service = self::getAdminService("get_num_instances"); $v = $service->apps_services_versions->get(self::getProjectId(), $module, $version); return $v->getManualScaling()->getInstances(); } catch (\Exception $e) { @@ -412,7 +417,7 @@ public static function setNumInstances($instances, try { $module = $module ?: self::getCurrentModuleName(); $version = $version ?: self::getCurrentVersionName(); - $service = self::getAdminService(); + $service = self::getAdminService("set_num_instances"); $v = new \Google_Service_Appengine_Version(); $manualScaling = new \Google_Service_Appengine_ManualScaling(); $manualScaling->setInstances($instances); @@ -483,7 +488,7 @@ public static function startVersion($module, $version) { $module = $module ?: self::getCurrentModuleName(); $version = $version ?: self::getCurrentVersionName(); try { - $service = self::getAdminService(); + $service = self::getAdminService("start_version"); $v = new \Google_Service_Appengine_Version(); $v->setServingStatus('SERVING'); $service->apps_services_versions->patch( @@ -542,7 +547,7 @@ public static function stopVersion($module = null, $version = null) { $module = $module ?: self::getCurrentModuleName(); $version = $version ?: self::getCurrentVersionName(); try { - $service = self::getAdminService(); + $service = self::getAdminService("stop_version"); $v = new \Google_Service_Appengine_Version(); $v->setServingStatus('STOPPED'); $service->apps_services_versions->patch( @@ -628,9 +633,7 @@ public static function getHostname($module = null, try { $services = self::getModules(); - $service = self::getAdminService(); - - // Fetch application details to get the default hostname + $service = self::getAdminService("get_hostname"); $app = $service->apps->get($projectId); $defaultHostname = $app->getDefaultHostname(); } catch (\Exception $e) { @@ -641,7 +644,6 @@ public static function getHostname($module = null, throw new ModulesException("Invalid Module"); } - // Handle Legacy Applications (Single 'default' module) if (count($services) === 1 && $services[0] === 'default') { if ($reqModule !== 'default') { throw new ModulesException("Module '$reqModule' not found."); @@ -651,7 +653,6 @@ public static function getHostname($module = null, : self::constructHostname($reqVersion, $defaultHostname); } - // Handle instance-specific hostname requests if ($instance !== null) { try { $vDetails = $service->apps_services_versions->get($projectId, $reqModule, $reqVersion, ['view' => 'FULL']); @@ -674,14 +675,12 @@ public static function getHostname($module = null, } } - // Handle requests with no explicit version and no instance if ($version === null) { try { $versionsList = self::getVersions($reqModule); if (in_array($reqVersion, $versionsList)) { return self::constructHostname($reqVersion, $reqModule, $defaultHostname); } else { - // Return hostname without version if current version doesn't exist in target module return self::constructHostname($reqModule, $defaultHostname); } } catch (\Google_Service_Exception $e) { @@ -692,7 +691,6 @@ public static function getHostname($module = null, } } - // Request with a version but no instance return self::constructHostname($version, $reqModule, $defaultHostname); } From 3f8342cbdb34955e6b23697c65c95850bb7a738f Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 27 Feb 2026 06:54:45 +0000 Subject: [PATCH 8/8] Fixing tests --- tests/Api/Modules/ModulesServiceTest.php | 86 +++++++++++++----------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/tests/Api/Modules/ModulesServiceTest.php b/tests/Api/Modules/ModulesServiceTest.php index 8cd29440..09ecd20f 100644 --- a/tests/Api/Modules/ModulesServiceTest.php +++ b/tests/Api/Modules/ModulesServiceTest.php @@ -99,7 +99,7 @@ public function testGetModulesAdminApiSuccess() { ->willReturn($response); // 4. Mock the main App Engine Service client - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; // Inject the mock @@ -120,7 +120,7 @@ public function testGetModulesAdminApiFailure() { $appsServices->method('listAppsServices') ->willThrowException(new \Exception("Admin API Error")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; ModulesService::setAdminServiceForTesting($adminService); @@ -170,7 +170,7 @@ public function testGetVersionsAdminApiSuccess() { ->willReturn($response); // 4. Mock the main App Engine Service client - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -198,7 +198,7 @@ public function testGetVersionsAdminApiDefaultModule() { ->with('test-project', 'default') ->willReturn($response); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -217,7 +217,7 @@ public function testGetVersionsAdminApiFailure() { $versionsResource->method('listAppsServicesVersions') ->willThrowException(new \Exception("Admin API list failure")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -280,7 +280,7 @@ public function testGetDefaultVersionAdminApiSuccess100Percent() { ->with('test-project', $targetModule) ->willReturn($serviceConfig); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; ModulesService::setAdminServiceForTesting($adminService); @@ -307,7 +307,7 @@ public function testGetDefaultVersionAdminApiSuccessSplit() { $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('get')->willReturn($serviceConfig); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; ModulesService::setAdminServiceForTesting($adminService); @@ -334,7 +334,7 @@ public function testGetDefaultVersionAdminApiSuccessTieBreak() { $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('get')->willReturn($serviceConfig); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; ModulesService::setAdminServiceForTesting($adminService); @@ -358,7 +358,7 @@ public function testGetDefaultVersionAdminApiNoAllocations() { $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('get')->willReturn($serviceConfig); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; ModulesService::setAdminServiceForTesting($adminService); @@ -379,7 +379,7 @@ public function testGetDefaultVersionAdminApiFailure() { $appsServices->method('get') ->willThrowException(new \Exception("Call to undefined function Google\AppEngine\Api\Modules\errorCodeToException()")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services = $appsServices; ModulesService::setAdminServiceForTesting($adminService); @@ -412,7 +412,7 @@ public function testGetNumInstancesAdminApiSuccess() { ->willReturn($version); // 3. Mock the main App Engine Service client - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -441,7 +441,7 @@ public function testGetNumInstancesAdminApiDefaults() { ->with('test-project', 'default-module', 'v2') // Expects parsed version 'v2' ->willReturn($version); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -460,7 +460,7 @@ public function testGetNumInstancesAdminApiFailure() { $versionsResource->method('get') ->willThrowException(new \Exception("Admin API Get Version Error")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -551,7 +551,7 @@ public function testSetNumInstancesAdminApiSuccess() { ); // 3. Mock the main App Engine Service client - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -580,7 +580,7 @@ public function testSetNumInstancesAdminApiDefaults() { ['updateMask' => 'manualScaling.instances'] ); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -598,7 +598,7 @@ public function testSetNumInstancesAdminApiFailure() { $versionsResource->method('patch') ->willThrowException(new \Exception("Admin API Patch Error")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -693,7 +693,7 @@ public function testStartVersionAdminApiSuccess() { ); // 3. Mock the main App Engine Service client - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -712,7 +712,7 @@ public function testStartVersionAdminApiFailure() { $versionsResource->method('patch') ->willThrowException(new \Exception("Admin API Patch Error")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -792,7 +792,7 @@ public function testStopVersionAdminApiSuccess() { ); // 3. Mock the main App Engine Service client - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -820,7 +820,7 @@ public function testStopVersionAdminApiDefaults() { ['updateMask' => 'servingStatus'] ); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -838,7 +838,7 @@ public function testStopVersionAdminApiFailure() { $versionsResource->method('patch') ->willThrowException(new \Exception("Admin API Patch Error")); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps_services_versions = $versionsResource; ModulesService::setAdminServiceForTesting($adminService); @@ -915,7 +915,7 @@ public function testGetHostnameAdminApiLegacyApp() { $appsResource = $this->createMock('Google_Service_Appengine_Resource_Apps'); $appsResource->method('get')->with('test-project')->willReturn($app); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps = $appsResource; // Mock getModules to return only 'default' @@ -947,7 +947,7 @@ public function testGetHostnameAdminApiManualScaling() { $app = $this->createMock('Google_Service_Appengine_Application'); $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps = $this->createMock('Google_Service_Appengine_Resource_Apps'); $adminService->apps->method('get')->willReturn($app); @@ -988,7 +988,7 @@ public function testGetHostnameAdminApiVersionFallback() { $app = $this->createMock('Google_Service_Appengine_Application'); $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); - $adminService = $this->createMock('Google_Service_Appengine'); + $adminService = $this->createAdminServiceMock(); $adminService->apps = $this->createMock('Google_Service_Appengine_Resource_Apps'); $adminService->apps->method('get')->willReturn($app); @@ -1016,28 +1016,31 @@ public function testGetHostnameAdminApiVersionFallback() { * Tests that getHostname fails if an instance is requested for a non-manually scaled service. */ public function testGetHostnameAdminApiInvalidScalingError() { - // Enable the Admin API path putenv('APPENGINE_MODULES_USE_ADMIN_API=true'); $_SERVER['GOOGLE_CLOUD_PROJECT'] = 'test-project'; - // 1. Mock the App Engine Application (for default hostname retrieval) + // 1. Mock the Application $app = $this->createMock('Google_Service_Appengine_Application'); $app->method('getDefaultHostname')->willReturn('myapp.appspot.com'); - $appsResource = $this->createMock('Google_Service_Appengine_Resource_Apps'); $appsResource->method('get')->with('test-project')->willReturn($app); - // 2. Mock the Services List (to prevent foreach(null) in getModules) - // The previous error occurred because this mock returned null by default. + // 2. Mock the Services List - MUST return 'm1' to avoid "Invalid Module" error + $serviceMock = $this->createMock('Google_Service_Appengine_Service'); + $serviceMock->method('getId')->willReturn('m1'); + $listServicesResponse = $this->createMock('Google_Service_Appengine_ListServicesResponse'); - $listServicesResponse->method('getServices')->willReturn([]); // Return empty array + $listServicesResponse->method('getServices')->willReturn([$serviceMock]); $appsServices = $this->createMock('Google_Service_Appengine_Resource_AppsServices'); $appsServices->method('listAppsServices')->willReturn($listServicesResponse); // 3. Mock a Version that is NOT manually scaled - // This triggers the specific error we are testing for. - $version = $this->createMock('Google_Service_Appengine_Version'); + // FIX: Use onlyMethods() instead of addMethods() + $version = $this->getMockBuilder('Google_Service_Appengine_Version') + ->disableOriginalConstructor() + ->onlyMethods(['getManualScaling']) + ->getMock(); $version->method('getManualScaling')->willReturn(null); $versionsResource = $this->createMock('Google_Service_Appengine_Resource_AppsServicesVersions'); @@ -1045,20 +1048,18 @@ public function testGetHostnameAdminApiInvalidScalingError() { ->with('test-project', 'm1', 'v1', ['view' => 'FULL']) ->willReturn($version); - // 4. Assemble the main Admin Service mock - $adminService = $this->createMock('Google_Service_Appengine'); + // 4. Assemble the main Admin Service mock using your helper + $adminService = $this->createAdminServiceMock(); // Ensures getClient() is not null $adminService->apps = $appsResource; $adminService->apps_services = $appsServices; $adminService->apps_services_versions = $versionsResource; - // Inject the mock into the service ModulesService::setAdminServiceForTesting($adminService); - // 5. Assert that the specific ModulesException is thrown + // 5. Assert that the specific Scaling error is thrown $this->expectException(ModulesException::class); - $this->expectExceptionMessage("Invalid Module"); + $this->expectExceptionMessage("Instance-specific hostnames are only available for manually scaled services."); - // Execute the call that should trigger the exception ModulesService::getHostname('m1', 'v1', 0); } @@ -1136,4 +1137,13 @@ public function testGetHostnameLegacyWithInvalidInstancesError() { $this->assertEquals('hostname', ModulesService::getHostname()); $this->apiProxyMock->verify(); } + + private function createAdminServiceMock() { + $client = $this->createMock('Google_Client'); + $adminService = $this->createMock('Google_Service_Appengine'); + + $adminService->method('getClient')->willReturn($client); + + return $adminService; +} }