From d099c018524e22b16b97395eaad4af654bf23536 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:02:38 -0700 Subject: [PATCH 1/3] Add Microsoft Graph token provider and tests Introduce MicrosoftGraphTokenProvider to obtain and cache OAuth2 client-credentials tokens from Microsoft Graph. The provider validates required credentials, posts to the tenant token endpoint via Guzzle, throws a RuntimeException on missing creds or HTTP errors (propagating server error messages), and caches the access token for ~50 minutes using a server-specific cache key. Includes unit tests that assert exception behavior for missing credentials and mock Guzzle to verify token request parameters and caching behavior. --- .../Mail/MicrosoftGraphTokenProvider.php | 61 ++++++++++++++++ .../Mail/MicrosoftGraphTokenProviderTest.php | 73 +++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 ProcessMaker/Mail/MicrosoftGraphTokenProvider.php create mode 100644 tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php new file mode 100644 index 0000000000..1926107806 --- /dev/null +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -0,0 +1,61 @@ +serverIndex ?: 'default'); + + return Cache::remember($cacheKey, now()->addMinutes(50), function () { + return $this->requestAccessToken(); + }); + } + + private function requestAccessToken(): string + { + $tenantId = $this->config['tenant_id'] ?? null; + $clientId = $this->config['key'] ?? null; + $clientSecret = $this->config['secret'] ?? null; + + if (!$tenantId || !$clientId || !$clientSecret) { + throw new RuntimeException('Microsoft Graph credentials are not configured.'); + } + + $client = new Client(); + $response = $client->post(sprintf(self::TOKEN_URL, $tenantId), [ + 'form_params' => [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'scope' => self::DEFAULT_SCOPE, + 'grant_type' => 'client_credentials', + ], + 'http_errors' => false, + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() >= 400) { + $message = $body['error_description'] ?? $body['error']['message'] ?? 'Unknown error'; + + throw new RuntimeException('Failed to get Microsoft Graph access token: ' . $message); + } + + return $body['access_token']; + } +} diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php new file mode 100644 index 0000000000..51f434b75e --- /dev/null +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -0,0 +1,73 @@ + '', + 'key' => 'client-id', + 'secret' => 'client-secret', + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Microsoft Graph credentials are not configured.'); + + $provider->getAccessToken(); + } + + public function testGetAccessTokenRequestsTokenFromMicrosoft() + { + Cache::flush(); + + $tokenResponseBody = Mockery::mock(); + $tokenResponseBody->shouldReceive('getContents') + ->andReturn(json_encode(['access_token' => 'token-from-azure'])); + + $tokenResponse = Mockery::mock(); + $tokenResponse->shouldReceive('getStatusCode')->andReturn(200); + $tokenResponse->shouldReceive('getBody')->andReturn($tokenResponseBody); + + $guzzle = Mockery::mock('overload:GuzzleHttp\Client'); + $guzzle->shouldReceive('post') + ->once() + ->with( + 'https://login.microsoftonline.com/tenant-id-123/oauth2/v2.0/token', + Mockery::on(function ($options) { + return $options['form_params']['client_id'] === 'client-id-123' + && $options['form_params']['client_secret'] === 'client-secret-123' + && $options['form_params']['scope'] === 'https://graph.microsoft.com/.default' + && $options['form_params']['grant_type'] === 'client_credentials' + && $options['http_errors'] === false; + }) + ) + ->andReturn($tokenResponse); + + $provider = new MicrosoftGraphTokenProvider([ + 'tenant_id' => 'tenant-id-123', + 'key' => 'client-id-123', + 'secret' => 'client-secret-123', + ], 1); + + $this->assertSame('token-from-azure', $provider->getAccessToken()); + $this->assertSame('token-from-azure', $provider->getAccessToken()); + } +} From 727173822d1bef83180ce54ed754d4127c07ba87 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:37:21 -0700 Subject: [PATCH 2/3] Inject HTTP client into token provider Add an optional Guzzle Client dependency to MicrosoftGraphTokenProvider so an HTTP client can be injected for testing. The constructor now accepts ?Client $httpClient and the provider uses $this->httpClient ?? new Client() when posting for tokens. Update the unit test to mock GuzzleHttp\Client (remove overload-based mock and separate-process annotations), import Client, and pass the mocked client into the provider. This enables dependency injection and simpler, more reliable tests. --- ProcessMaker/Mail/MicrosoftGraphTokenProvider.php | 5 +++-- .../Mail/MicrosoftGraphTokenProviderTest.php | 9 +++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php index 1926107806..913274bea6 100644 --- a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -14,7 +14,8 @@ class MicrosoftGraphTokenProvider public function __construct( private array $config, - private int|string $serverIndex = 0 + private int|string $serverIndex = 0, + private ?Client $httpClient = null, ) { } @@ -37,7 +38,7 @@ private function requestAccessToken(): string throw new RuntimeException('Microsoft Graph credentials are not configured.'); } - $client = new Client(); + $client = $this->httpClient ?? new Client(); $response = $client->post(sprintf(self::TOKEN_URL, $tenantId), [ 'form_params' => [ 'client_id' => $clientId, diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php index 51f434b75e..4c5870934b 100644 --- a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -2,16 +2,13 @@ namespace Tests\Unit\ProcessMaker\Mail; +use GuzzleHttp\Client; use Illuminate\Support\Facades\Cache; use Mockery; use ProcessMaker\Mail\MicrosoftGraphTokenProvider; use RuntimeException; use Tests\TestCase; -/** - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled - */ class MicrosoftGraphTokenProviderTest extends TestCase { protected function tearDown(): void @@ -46,7 +43,7 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() $tokenResponse->shouldReceive('getStatusCode')->andReturn(200); $tokenResponse->shouldReceive('getBody')->andReturn($tokenResponseBody); - $guzzle = Mockery::mock('overload:GuzzleHttp\Client'); + $guzzle = Mockery::mock(Client::class); $guzzle->shouldReceive('post') ->once() ->with( @@ -65,7 +62,7 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() 'tenant_id' => 'tenant-id-123', 'key' => 'client-id-123', 'secret' => 'client-secret-123', - ], 1); + ], 1, $guzzle); $this->assertSame('token-from-azure', $provider->getAccessToken()); $this->assertSame('token-from-azure', $provider->getAccessToken()); From b0d14ca501733e5af85d67b9b7f925ebdf62e8b3 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:40:11 -0700 Subject: [PATCH 3/3] Use real PSR-7 Response in test Replace Mockery-based response mocks in MicrosoftGraphTokenProviderTest with an actual GuzzleHttp\Psr7\Response instance. Simplifies the test by providing a real PSR-7 response (200) with a JSON body and removes the need to mock getBody()/getContents() and getStatusCode(). Added the Response use statement. --- .../Mail/MicrosoftGraphTokenProviderTest.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php index 4c5870934b..a4f62d67d5 100644 --- a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\ProcessMaker\Mail; use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response; use Illuminate\Support\Facades\Cache; use Mockery; use ProcessMaker\Mail\MicrosoftGraphTokenProvider; @@ -35,13 +36,7 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() { Cache::flush(); - $tokenResponseBody = Mockery::mock(); - $tokenResponseBody->shouldReceive('getContents') - ->andReturn(json_encode(['access_token' => 'token-from-azure'])); - - $tokenResponse = Mockery::mock(); - $tokenResponse->shouldReceive('getStatusCode')->andReturn(200); - $tokenResponse->shouldReceive('getBody')->andReturn($tokenResponseBody); + $tokenResponse = new Response(200, [], json_encode(['access_token' => 'token-from-azure'])); $guzzle = Mockery::mock(Client::class); $guzzle->shouldReceive('post')