setPrivateTokenField('app_token', $appSecretToken); $this->setPrivateTokenField('appAccessGrant', $primaryGrant); $this->setPrivateTokenField('appAccessGrant2', $secondaryGrant); } public function resolveAgreementGrantToken(bool $authToken2): string { return $this->resolve_agreement_grant_token($authToken2); } public function resolveAppSecretToken(): string { return $this->resolve_app_secret_token(); } public function assertSuccessfulResponse(int $httpStatusCode, string|false $response): string { return $this->assert_successful_response($httpStatusCode, $response); } private function setPrivateTokenField(string $fieldName, string $value): void { $reflection = new ReflectionProperty(economic_m::class, $fieldName); $reflection->setValue($this, $value); } } } beforeEach(function (): void { global $ECONOMIC_API; $ECONOMIC_API = [ 'app_secret_token' => 'test-secret', 'app_access_grant' => 'primary-grant', 'app_access_grant2' => 'secondary-grant', ]; }); it('uses grant #2 in economic_endpoint_t when explicitly requested and configured', function (): void { $probe = new EconomicEndpointGrantFallbackProbe(); expect($probe->resolveAgreementGrantToken(true))->toBe('secondary-grant'); expect($probe->resolveAgreementGrantToken(false))->toBe('primary-grant'); }); it('falls back to grant #1 in economic_endpoint_t when grant #2 is missing', function (): void { global $ECONOMIC_API; $ECONOMIC_API['app_access_grant2'] = ''; $probe = new EconomicEndpointGrantFallbackProbe(); expect($probe->resolveAgreementGrantToken(true))->toBe('primary-grant'); expect($probe->resolveAgreementGrantToken(false))->toBe('primary-grant'); }); it('throws for missing required primary grant in economic_endpoint_t', function (): void { global $ECONOMIC_API; $ECONOMIC_API['app_access_grant'] = ''; $probe = new EconomicEndpointGrantFallbackProbe(); expect(fn() => $probe->resolveAgreementGrantToken(false)) ->toThrow(RuntimeException::class, 'Missing e-conomic agreement grant token'); }); it('throws for missing app secret in economic_endpoint_t', function (): void { global $ECONOMIC_API; $ECONOMIC_API['app_secret_token'] = ''; $probe = new EconomicEndpointGrantFallbackProbe(); expect(fn() => $probe->resolveAppSecretToken()) ->toThrow(RuntimeException::class, 'Missing e-conomic app secret token'); }); it('uses grant #2 in legacy economic_m when available', function (): void { $probe = new EconomicMLegacyGrantFallbackProbe(); $probe->configureTokens('test-secret', 'primary-grant', 'secondary-grant'); expect($probe->resolveAgreementGrantToken(true))->toBe('secondary-grant'); expect($probe->resolveAgreementGrantToken(false))->toBe('primary-grant'); }); it('falls back to grant #1 in legacy economic_m when grant #2 is missing', function (): void { $probe = new EconomicMLegacyGrantFallbackProbe(); $probe->configureTokens('test-secret', 'primary-grant', ''); expect($probe->resolveAgreementGrantToken(true))->toBe('primary-grant'); }); it('throws for missing required primary grant in legacy economic_m', function (): void { $probe = new EconomicMLegacyGrantFallbackProbe(); $probe->configureTokens('test-secret', '', 'secondary-grant'); expect(fn() => $probe->resolveAgreementGrantToken(false)) ->toThrow(RuntimeException::class, 'Missing e-conomic agreement grant token'); }); it('throws for missing app secret in legacy economic_m', function (): void { $probe = new EconomicMLegacyGrantFallbackProbe(); $probe->configureTokens('', 'primary-grant', 'secondary-grant'); expect(fn() => $probe->resolveAppSecretToken()) ->toThrow(RuntimeException::class, 'Missing e-conomic app secret token'); }); it('throws deterministic upstream exceptions for non-2xx endpoint trait responses', function (): void { $probe = new EconomicEndpointGrantFallbackProbe(); $errorPayload = json_encode([ 'message' => 'Could not parse query string filter.', 'errors' => ['Filtering is not allowed on property \'invalidField\'.'], 'logId' => 'abc123', 'httpStatusCode' => 400, ]); expect(fn() => $probe->assertSuccessfulResponse(400, $errorPayload)) ->toThrow(RuntimeException::class, 'Could not parse query string filter.'); expect(fn() => $probe->assertSuccessfulResponse(502, 'Gateway timeout')) ->toThrow(RuntimeException::class, 'HTTP 502'); }); it('throws deterministic upstream exceptions for non-2xx legacy economic_m responses', function (): void { $probe = new EconomicMLegacyGrantFallbackProbe(); $probe->configureTokens('test-secret', 'primary-grant', 'secondary-grant'); $errorPayload = json_encode([ 'message' => 'Could not parse query string filter.', 'errors' => ['Filtering is not allowed on property \'invalidField\'.'], 'logId' => 'abc123', 'httpStatusCode' => 400, ]); expect(fn() => $probe->assertSuccessfulResponse(400, $errorPayload)) ->toThrow(RuntimeException::class, 'Could not parse query string filter.'); expect(fn() => $probe->assertSuccessfulResponse(500, 'server error')) ->toThrow(RuntimeException::class, 'HTTP 500'); }); it('returns raw payload unchanged on successful HTTP statuses', function (): void { $endpointProbe = new EconomicEndpointGrantFallbackProbe(); $legacyProbe = new EconomicMLegacyGrantFallbackProbe(); $legacyProbe->configureTokens('test-secret', 'primary-grant', 'secondary-grant'); $payload = '{"collection":[{"customerNumber":1000}],"pagination":{"results":1}}'; expect($endpointProbe->assertSuccessfulResponse(200, $payload))->toBe($payload); expect($legacyProbe->assertSuccessfulResponse(200, $payload))->toBe($payload); }); it('bounds e-conomic curl calls below the PHP request timeout', function (): void { $legacyContent = file_get_contents(app_path('modules/economic/economic_m.php')); $endpointContent = file_get_contents(app_path('traits/economic_endpoint_t.php')); expect($legacyContent)->not->toBeFalse() ->and($endpointContent)->not->toBeFalse(); foreach ([(string)$legacyContent, (string)$endpointContent] as $content) { expect($content)->toContain('CURLOPT_CONNECTTIMEOUT => 3') ->and($content)->toContain('CURLOPT_TIMEOUT => 30') ->and($content)->not->toContain('CURLOPT_TIMEOUT => 0'); } });