[Web][Dovecot] imapsync: update to 2.314, refresh OAuth tokens of running jobs, add source presets

This commit is contained in:
FreddleSpl0it committed 2026-10-22 18:27:49 +02:00
1 parent 474f1c688b
commit c145f858e2
15 files changed
+7060 -3406

No files matched your search

@@ -85,6 +85,20 @@ class ApiControllerTest extends TestCase
$this->assertSame('*', $_SESSION['return'][0]['log'][3]['oauth_client_secret']);
}
public function testSavingAClientCredentialsSourceReportsAFailedFirstToken(): void
{
$source = Source::fromRow(['id' => 5, 'name' => 'm365', 'auth_type' => 'XOAUTH2', 'oauth_last_refresh_error' => 'HTTP 401: invalid_client']);
$this->sources->method('create')->willReturn($source);
$this->sources->method('update')->willReturn($source);
$this->assertTrue($this->api->addSource(['name' => 'm365']));
$this->assertTrue($this->api->editSources([5], ['name' => 'm365']));
$failed = ['danger', ['imapsync_source_token_refresh_failed', 'm365 (HTTP 401: invalid_client)']];
$this->assertSame([$failed, ['success', ['imapsync_source_added', 'm365']], $failed, ['success', ['imapsync_source_modified', 'm365']]],
$this->messages());
}
public function testEditJobsKeepsBatchMessageOrder(): void
{
$this->jobs->method('update')->willReturnCallback(function (int $id, array $data): Job {
@@ -75,12 +75,29 @@ class OAuthServiceTest extends TestCase
$this->client->method('isAllowed')->willReturnOnConsecutiveCalls(true, false);
$source = self::source(['oauth_flow' => 'authorization_code']);
$this->assertSame('https://login.example.org/authorize?tenant=x&client_id=client&response_type=code'
$this->assertSame('https://login.example.org/authorize?tenant=x&prompt=select_account&client_id=client&response_type=code'
. '&redirect_uri=https%3A%2F%2Fmail.example.org%2Fsyncjob-oauth&response_mode=query&scope=scope&state=abc',
$this->service()->authorizeUrl($source, 'abc'));
$this->assertNull($this->service()->authorizeUrl($source, 'abc'));
}
public function testSignInParamsGoToTheAuthorizeUrlOnly(): void
{
$this->client->method('isAllowed')->willReturn(true);
$this->client->expects($this->once())->method('request')->with(
'https://login.example.org/token',
true,
['resource' => 'r', 'client_id' => 'client', 'client_secret' => 'secret', 'scope' => 'scope', 'grant_type' => 'client_credentials']
)->willReturn(['access_token' => 'new']);
$extra = '{"prompt":"consent select_account","access_type":"offline","resource":"r"}';
// prompt overrides the default; sign-in params never reach the token endpoint and vice versa
$this->assertSame('https://login.example.org/authorize?tenant=x&prompt=consent+select_account&access_type=offline&client_id=client'
. '&response_type=code&redirect_uri=https%3A%2F%2Fmail.example.org%2Fsyncjob-oauth&response_mode=query&scope=scope&state=abc',
$this->service()->authorizeUrl(self::source(['oauth_flow' => 'authorization_code', 'oauth_extra_params' => $extra]), 'abc'));
$this->assertNull($this->service()->refreshSourceToken(self::source(['oauth_extra_params' => $extra])));
}
public function testClientCredentialsRefreshStoresToken(): void
{
$this->client->expects($this->once())->method('request')->with(
@@ -152,12 +169,13 @@ class OAuthServiceTest extends TestCase
public function testRefreshDueAppliesExpiryAndRetryRules(): void
{
$soon = time() + 60;
// due 20 min ahead, retried 3 min after a failure
$soon = time() + 1100;
$this->tokens->method('refreshableSources')->willReturn([
self::source(['id' => 1, 'name' => 'no token']),
self::source(['id' => 2, 'name' => 'valid', 'oauth_token_expires' => time() + 3600]),
self::source(['id' => 3, 'name' => 'failed recently', 'oauth_token_expires' => $soon, 'oauth_last_refresh_error' => 'HTTP 500', 'refresh_attempt_age' => 900]),
self::source(['id' => 4, 'name' => 'failed long ago', 'oauth_token_expires' => $soon, 'oauth_last_refresh_error' => 'HTTP 500', 'refresh_attempt_age' => 901]),
self::source(['id' => 2, 'name' => 'valid', 'oauth_token_expires' => time() + 1300]),
self::source(['id' => 3, 'name' => 'failed recently', 'oauth_token_expires' => $soon, 'oauth_last_refresh_error' => 'HTTP 500', 'refresh_attempt_age' => 180]),
self::source(['id' => 4, 'name' => 'failed long ago', 'oauth_token_expires' => $soon, 'oauth_last_refresh_error' => 'HTTP 500', 'refresh_attempt_age' => 181]),
]);
$this->tokens->method('deleteUnusedOlderThan')->with(86400)->willReturn(0);
$this->tokens->method('refreshableUserTokens')->willReturn([]);
@@ -429,6 +429,23 @@ class SourceServiceTest extends TestCase
$this->assertSame('src', $service->refreshToken(7)->name);
}
public function testSavedClientCredentialsSourcesFetchTheirFirstToken(): void
{
$this->repo->method('findVisible')->willReturnOnConsecutiveCalls(self::oauthSource(), self::oauthSource(['active' => 0]));
$this->client->expects($this->exactly(2))->method('request')->willReturnOnConsecutiveCalls(
['access_token' => 'first', 'expires_in' => 3600],
$this->throwException(new OAuthException('HTTP 401: invalid_client'))
);
$this->tokens->expects($this->once())->method('storeSourceToken');
$this->tokens->expects($this->once())->method('storeSourceError')->with(7, 'HTTP 401: invalid_client');
$service = $this->service(FakeContext::admin());
$this->assertNull($service->create($this->oauthInput())->lastRefreshError);
$this->assertSame('HTTP 401: invalid_client', $service->update(7, ['oauth_scope' => 'new-scope'])->lastRefreshError);
// inactive sources get no token
$service->update(7, ['oauth_scope' => 'other-scope']);
}
public function testConnectableSourceIsActiveAuthorizationCode(): void
{
$this->repo->method('findVisible')->willReturnOnConsecutiveCalls(