Skip to content

Commit b2e5f45

Browse files
authored
fix(external-services): argument error (#521)
implode(): Argument #2 ($array) must be of type ?array, string given
1 parent b4afe57 commit b2e5f45

File tree

3 files changed

+89
-3
lines changed

3 files changed

+89
-3
lines changed

.github/workflows/push.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
- { name: "AuditEventTypesTest", filter: "--filter AuditEventTypesTest" }
6464
- { name: "GuzzleTracingTest", filter: "--filter GuzzleTracingTest" }
6565
- { name: "Repositories", filter: "--filter tests/Repositories/" }
66+
- { name: "Services", filter: "--filter tests/Unit/Services/" }
6667
env:
6768
OTEL_SERVICE_ENABLED: false
6869
APP_ENV: testing

app/Services/Apis/AbstractOAuth2Api.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ protected function getAccessToken():?string{
132132
Log::debug("AbstractOAuth2Api::getAccessToken - access token is empty, getting new one");
133133
$client = $this->getIDPClient();
134134
$appConfig = $this->getAppConfig();
135-
$scopes = $appConfig['scopes'] ?? [];
136-
Log::debug(sprintf( "AbstractOAuth2Api::getAccessToken - got scopes %s", implode(' ', $scopes)));
135+
$scopes = $appConfig['scopes'] ?? '';
136+
Log::debug(sprintf( "AbstractOAuth2Api::getAccessToken - got scopes %s", $scopes));
137137
// Try to get an access token using the client credentials grant.
138138
$accessToken = $client->getAccessToken('client_credentials', ['scope' => $scopes]);
139139
$token = $accessToken->getToken();
@@ -163,4 +163,4 @@ protected function getAccessToken():?string{
163163
protected function cleanAccessToken():void{
164164
$this->cacheService->delete($this->getAccessTokenCacheKey());
165165
}
166-
}
166+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php namespace Tests\Unit\Services;
2+
/**
3+
* Copyright 2026 OpenStack Foundation
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
**/
14+
use App\Services\Apis\MailApi;
15+
use Illuminate\Support\Facades\Config;
16+
use libs\utils\ICacheService;
17+
use Mockery;
18+
use Tests\TestCase;
19+
20+
/**
21+
* Class AbstractOAuth2ApiScopesTest
22+
* Regression test for implode() TypeError when scopes config is a string.
23+
* @see https://github.com/OpenStackweb/summit-api/issues/XXX
24+
* @package Tests\Unit\Services
25+
*/
26+
class AbstractOAuth2ApiScopesTest extends TestCase
27+
{
28+
protected function tearDown(): void
29+
{
30+
Mockery::close();
31+
parent::tearDown();
32+
}
33+
34+
/**
35+
* Regression test: getAccessToken() must not throw TypeError when
36+
* scopes is a string from env(). Previously, implode(' ', $scopes)
37+
* crashed because $scopes was a string, not an array.
38+
*
39+
* @dataProvider scopesProvider
40+
*/
41+
public function testGetAccessTokenHandlesVariousScopesTypes($scopeValue, string $description)
42+
{
43+
Config::set('idp.authorization_endpoint', 'https://idp.test/authorize');
44+
Config::set('idp.token_endpoint', 'https://idp.test/token');
45+
Config::set('mail.service_base_url', 'https://mail.test');
46+
Config::set('mail.service_client_id', 'test-client');
47+
Config::set('mail.service_client_secret', 'test-secret');
48+
Config::set('mail.service_client_scopes', $scopeValue);
49+
Config::set('curl.timeout', 1);
50+
Config::set('curl.allow_redirects', false);
51+
Config::set('curl.verify_ssl_cert', true);
52+
53+
$cacheService = Mockery::mock(ICacheService::class);
54+
$cacheService->shouldReceive('getSingleValue')->andReturn(null);
55+
56+
$api = new MailApi($cacheService);
57+
58+
$reflection = new \ReflectionMethod($api, 'getAccessToken');
59+
$reflection->setAccessible(true);
60+
61+
try {
62+
$reflection->invoke($api);
63+
} catch (\TypeError $e) {
64+
$this->fail("TypeError thrown with {$description}: " . $e->getMessage());
65+
} catch (\Exception $e) {
66+
// Connection/HTTP errors are expected since IDP is not reachable.
67+
// The critical assertion is that no TypeError was thrown from implode().
68+
$this->assertNotInstanceOf(
69+
\TypeError::class,
70+
$e,
71+
"No TypeError should occur with {$description}"
72+
);
73+
}
74+
}
75+
76+
public static function scopesProvider(): array
77+
{
78+
return [
79+
'string scopes (space-separated)' => ['scope1 scope2', 'space-separated string scopes'],
80+
'single string scope' => ['payment-profile/read', 'single string scope'],
81+
'null scopes' => [null, 'null scopes'],
82+
'empty string scopes' => ['', 'empty string scopes'],
83+
];
84+
}
85+
}

0 commit comments

Comments
 (0)