[Web] API: complete the OpenAPI docs, add route tests for json_api.php

This commit is contained in:
FreddleSpl0it committed 2026-11-10 20:04:05 +01:00
1 parent 9825c8d290
commit 2418da031f
47 files changed
+18848 -7491

No files matched your search

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Mailcow\Tests\Api\Modules;
use Mailcow\Tests\Api\ApiTestCase;
/** Routes identity-provider, identity-provider-test, identity-provider-sync and identity-link */
class IdentityRoutesTest extends ApiTestCase
{
private const API = 'Mailcow\Identity\IdentityModule->api->';
/** The Identity Provider settings that prerequisites.inc.php loads into $iam_settings */
private const IAM_SETTINGS = ['iam_settings' => [
'authsource' => 'keycloak',
'server_url' => 'https://auth.example.org',
'realm' => 'mailcow',
'client_id' => 'mailcow',
'client_secret' => 'client-secret-value',
'bindpass' => 'bind-password',
'access_token' => 'cached-token',
'redirect_url_extra' => ['https://webmail.example.org'],
'login_provisioning' => true,
]];
private const SETTINGS = [
'authsource' => 'keycloak',
'server_url' => 'https://auth.example.org',
'realm' => 'mailcow',
'client_id' => 'mailcow',
'client_secret' => 'client-secret-value',
'bindpass' => 'bind-password',
'access_token' => 'cached-token',
'redirect_url_extra' => ['https://webmail.example.org'],
'login_provisioning' => true,
];
public function testSettingsForReadWriteKeys(): void
{
$result = $this->get('get/identity-provider', ['globals' => self::IAM_SETTINGS]);
$this->assertSame(self::SETTINGS, $result->json());
$this->assertNotCalled($result, self::API . 'getStatus');
}
public function testReadOnlyKeysGetNoSecrets(): void
{
$result = $this->get('get/identity-provider', ['globals' => self::IAM_SETTINGS, 'session' => self::API_RO]);
$this->assertSame(array_replace(self::SETTINGS, ['client_secret' => '*', 'bindpass' => '*', 'access_token' => '*']), $result->json());
}
public function testSettingsOnlyForAdmins(): void
{
foreach ([self::DOMAIN_ADMIN, self::USER] as $session) {
$this->assertSame('{}', $this->get('get/identity-provider', ['globals' => self::IAM_SETTINGS, 'session' => $session])->body);
}
}
public function testOtherObjectsAnswerTheSettingsToo(): void
{
$result = $this->get('get/identity-provider/anything', ['globals' => self::IAM_SETTINGS]);
$this->assertSame(self::SETTINGS, $result->json());
}
public function testSyncStatus(): void
{
$status = ['provider' => 'keycloak', 'can_sync' => true, 'sync_enabled' => false, 'counts' => ['accounts' => 2], 'runs' => []];
$result = $this->get('get/identity-provider/sync-status', ['session' => self::API_RO, 'script' => [self::answer(self::API . 'getStatus', [], $status)]]);
$this->assertCalled($result, self::API . 'getStatus', []);
$this->assertSame($status, $result->json());
}
public function testLink(): void
{
$link = ['provider' => 'keycloak', 'external_id' => '8d5da28d-7541-428b-8356-a9704f73e7f4', 'idp_disabled' => 0, 'missing_since' => null];
$result = $this->get('get/identity-link/alice@example.org', ['script' => [self::answer(self::API . 'getLink', ['alice@example.org'], $link)]]);
$this->assertCalled($result, self::API . 'getLink', ['alice@example.org']);
$this->assertSame($link, $result->json());
$none = $this->get('get/identity-link');
$this->assertCalled($none, self::API . 'getLink', ['']);
$this->assertSame('{}', $none->body);
}
public function testEditSettings(): void
{
$attr = ['authsource' => 'ldap', 'host' => 'ldap.example.org', 'basedn' => 'dc=example,dc=org', 'binddn' => 'cn=mailcow', 'bindpass' => 'secret'];
$messages = [self::message('success', ['object_modified', ''], ['identity_provider', 'edit', ['bindpass' => '*']])];
$result = $this->post('edit/identity-provider', ['items' => ['identity-provider'], 'attr' => $attr],
['script' => [self::answer('identity_provider', ['edit'], true, $messages)]]);
$this->assertCalled($result, 'identity_provider', ['edit', $attr]);
$this->assertSame($messages, $result->json());
}
public function testConnectionTestAnswersWithoutDetails(): void
{
$attr = ['authsource' => 'keycloak', 'server_url' => 'https://auth.example.org', 'realm' => 'mailcow', 'client_id' => 'mailcow', 'client_secret' => 'secret'];
$ok = $this->post('edit/identity-provider-test', ['attr' => $attr], ['script' => [self::answer('identity_provider', ['test'], true)]]);
$this->assertCalled($ok, 'identity_provider', ['test', $attr]);
$this->assertSame(['type' => 'success', 'msg' => 'Task completed'], $ok->json());
$failed = $this->post('edit/identity-provider-test', ['attr' => $attr], ['script' => [self::answer('identity_provider', ['test'], false)]]);
$this->assertSame(['type' => 'error', 'msg' => 'Cannot edit item'], $failed->json());
}
public function testRequestSync(): void
{
$result = $this->post('edit/identity-provider-sync', ['items' => ['sync'], 'attr' => ['mode' => 'preview']]);
$this->assertCalled($result, self::API . 'requestRun', [['mode' => 'preview']]);
}
public function testDeleteSettingsIgnoresTheBody(): void
{
$result = $this->post('delete/identity-provider', ['anything']);
$this->assertSame([['delete']], $result->callsTo('identity_provider'));
}
public function testDeleteLinks(): void
{
$messages = [self::message('success', ['iam_link_removed', 'alice@example.org'], ['identity_provider', 'delete', 'link', ['alice@example.org']])];
$result = $this->post('delete/identity-link', ['alice@example.org'], ['script' => [self::answer(self::API . 'unlink', [], true, $messages)]]);
$this->assertCalled($result, self::API . 'unlink', [['alice@example.org']]);
$this->assertSame($messages, $result->json());
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Mailcow\Tests\Api\Modules;
use Mailcow\Tests\Api\ApiTestCase;
/** Routes mailbox-group: json_api.php hands them to the MailboxGroup module and passes its answers through */
class MailboxGroupRoutesTest extends ApiTestCase
{
private const API = 'Mailcow\MailboxGroup\MailboxGroupModule->api->';
public function testAdd(): void
{
$attr = ['domain' => 'example.org', 'name' => 'Sales', 'members' => ['alice@example.org'], 'sender_allowed' => 1];
$messages = [
self::message('success', ['group_added', 'Sales'], ['mailbox_group', 'add', 'group', $attr]),
self::message('warning', ['group_imap_grant_failed', 'shared@example.org'], ['mailbox_group', 'add', 'group', $attr]),
];
$result = $this->post('add/mailbox-group', $attr, ['script' => [self::answer(self::API . 'addGroup', [], true, $messages)]]);
$this->assertCalled($result, self::API . 'addGroup', [$attr]);
$this->assertSame($messages, $result->json());
}
public function testGetAll(): void
{
$groups = [['id' => 7, 'domain' => 'example.org', 'name' => 'Sales', 'can_edit' => true]];
$result = $this->get('get/mailbox-group/all', ['script' => [self::answer(self::API . 'getGroups', [], $groups)]]);
$this->assertCalled($result, self::API . 'getGroups', [null]);
$this->assertSame($groups, $result->json());
}
public function testGetAllOfDomains(): void
{
$result = $this->get('get/mailbox-group/all/example.org,example.com');
$this->assertCalled($result, self::API . 'getGroups', ['example.org,example.com']);
}
public function testNoGroupsAnswerAnEmptyObject(): void
{
$result = $this->get('get/mailbox-group/all', ['script' => [self::answer(self::API . 'getGroups', [], [])]]);
$this->assertSame('{}', $result->body);
}
public function testGetMemberships(): void
{
$memberships = ['groups' => [['id' => 7, 'name' => 'Sales', 'source' => 'manual']], 'template' => 'Staff', 'send_as' => ['sales@example.org']];
$result = $this->get('get/mailbox-group/member/alice@example.org', ['script' => [self::answer(self::API . 'getMemberships', [], $memberships)]]);
$this->assertCalled($result, self::API . 'getMemberships', ['alice@example.org']);
$this->assertSame($memberships, $result->json());
$this->assertNotCalled($result, self::API . 'getGroup');
}
public function testGetMembershipsWithoutMailbox(): void
{
$result = $this->get('get/mailbox-group/member');
$this->assertCalled($result, self::API . 'getMemberships', ['']);
$this->assertSame('{}', $result->body);
}
public function testGetOne(): void
{
$group = ['id' => 7, 'domain' => 'example.org', 'name' => 'Sales'];
$result = $this->get('get/mailbox-group/7', ['script' => [self::answer(self::API . 'getGroup', ['7'], $group)]]);
$this->assertCalled($result, self::API . 'getGroup', ['7']);
$this->assertSame($group, $result->json());
}
public function testEdit(): void
{
$attr = ['add_members' => ['carol@example.org'], 'remove_members' => 'bob@example.org'];
$result = $this->post('edit/mailbox-group', ['items' => [7, '8'], 'attr' => $attr]);
$this->assertCalled($result, self::API . 'editGroups', [[7, '8'], $attr]);
}
public function testEditWithASingleIdOrWithoutItems(): void
{
$single = $this->post('edit/mailbox-group', ['items' => 7, 'attr' => ['active' => 0]]);
$this->assertCalled($single, self::API . 'editGroups', [[7], ['active' => 0]]);
$none = $this->post('edit/mailbox-group', ['attr' => ['id' => [9], 'active' => 0]]);
$this->assertCalled($none, self::API . 'editGroups', [[], ['id' => [9], 'active' => 0]]);
}
public function testDelete(): void
{
$messages = [
self::message('success', ['group_removed', 'Sales'], ['mailbox_group', 'delete', 'group', ['id' => [7, 'x']]]),
self::message('danger', 'access_denied', ['mailbox_group', 'delete', 'group', ['id' => [7, 'x']]]),
];
$result = $this->post('delete/mailbox-group', [7, 'x'], ['script' => [self::answer(self::API . 'deleteGroups', [], true, $messages)]]);
$this->assertCalled($result, self::API . 'deleteGroups', [[7, 'x']]);
$this->assertSame($messages, $result->json());
}
}
+194
View File
@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Mailcow\Tests\Api\Modules;
use Mailcow\Tests\Api\ApiTestCase;
/** Routes syncjob, syncjobs, syncjob_source, syncjob_options and imapsync_settings: handed to the Imapsync module */
class SyncJobRoutesTest extends ApiTestCase
{
private const API = 'Mailcow\Imapsync\ImapsyncModule->api->';
public function testAddJob(): void
{
$attr = ['username' => 'alice@example.org', 'source_id' => 3, 'user1' => 'alice@example.com', 'password1' => 'secret', 'mins_interval' => 20];
$messages = [self::message('success', ['mailbox_modified', 'alice@example.org'], ['syncjob', 'add', 'job', ['password1' => '*']])];
$result = $this->post('add/syncjob', $attr, ['script' => [self::answer(self::API . 'addJob', [], true, $messages)]]);
$this->assertCalled($result, self::API . 'addJob', [$attr]);
$this->assertSame($messages, $result->json());
}
public function testAddSource(): void
{
$attr = ['name' => 'Old provider', 'host' => 'imap.example.com', 'port' => 993, 'enc' => 'SSL', 'scope' => 'domain', 'domains' => ['example.org']];
$result = $this->post('add/syncjob_source', $attr);
$this->assertCalled($result, self::API . 'addSource', [$attr]);
}
public function testAllJobsOfAnApiKey(): void
{
$jobs = [['id' => 12, 'user2' => 'alice@example.org', 'log' => 'done']];
$result = $this->get('get/syncjobs/all', ['script' => [self::answer(self::API . 'getAllJobs', [], $jobs)]]);
$this->assertCalled($result, self::API . 'getAllJobs', [null, []]);
$this->assertNotCalled($result, 'mailbox');
$this->assertSame($jobs, $result->json());
}
public function testAllJobsWithoutLog(): void
{
$result = $this->get('get/syncjobs/all/no_log');
$this->assertCalled($result, self::API . 'getAllJobs', ['no_log', []]);
$this->assertSame('{}', $result->body);
}
public function testAllJobsOfADomainAdminAreTheJobsOfHisMailboxes(): void
{
$result = $this->get('get/syncjobs/all/no_log', ['session' => self::DOMAIN_ADMIN, 'script' => [
self::answer('mailbox', ['get', 'domains'], ['example.org', 'example.com']),
self::answer('mailbox', ['get', 'mailboxes', 'example.org'], ['alice@example.org', 'bob@example.org']),
self::answer('mailbox', ['get', 'mailboxes', 'example.com'], false),
]]);
$this->assertCalled($result, self::API . 'getAllJobs', ['no_log', ['alice@example.org', 'bob@example.org']]);
}
public function testAllJobsOfAUser(): void
{
$result = $this->get('get/syncjobs/all', ['session' => self::USER, 'script' => [self::answer('mailbox', ['get', 'domains'], false)]]);
$this->assertCalled($result, self::API . 'getAllJobs', [null, []]);
}
public function testJobsOfAMailbox(): void
{
$withLog = $this->get('get/syncjobs/alice@example.org');
$this->assertCalled($withLog, self::API . 'getJobs', ['alice@example.org', null]);
$this->assertNotCalled($withLog, self::API . 'getAllJobs');
$withoutLog = $this->get('get/syncjobs/alice@example.org/no_log');
$this->assertCalled($withoutLog, self::API . 'getJobs', ['alice@example.org', 'no_log']);
$own = $this->get('get/syncjobs');
$this->assertCalled($own, self::API . 'getJobs', [null, null]);
}
public function testSources(): void
{
$sources = [['id' => 3, 'name' => 'Old provider', 'has_client_secret' => false]];
$all = $this->get('get/syncjob_source/all', ['script' => [self::answer(self::API . 'getSources', [], $sources)]]);
$this->assertCalled($all, self::API . 'getSources', []);
$this->assertSame($sources, $all->json());
$one = $this->get('get/syncjob_source/3', ['script' => [self::answer(self::API . 'getSource', ['3'], $sources[0])]]);
$this->assertCalled($one, self::API . 'getSource', ['3']);
$this->assertSame($sources[0], $one->json());
$this->assertSame('{}', $this->get('get/syncjob_source/99')->body);
}
public function testOptions(): void
{
$options = ['folder' => true, 'syncinternaldates' => false, 'sslargs1' => '^SSL_verify_mode=[01]$'];
$result = $this->get('get/syncjob_options', ['session' => self::USER, 'script' => [self::answer(self::API . 'getOptions', [], $options)]]);
$this->assertCalled($result, self::API . 'getOptions', []);
$this->assertSame($options, $result->json());
}
public function testSettings(): void
{
$settings = ['max_parallel' => 3, 'max_bytes_per_second' => 2048000, 'max_kb_per_second' => 2000];
$result = $this->get('get/imapsync_settings', ['script' => [self::answer(self::API . 'getSettings', [], $settings)]]);
$this->assertCalled($result, self::API . 'getSettings', []);
$this->assertSame($settings, $result->json());
$this->assertSame('{}', $this->get('get/imapsync_settings', ['session' => self::DOMAIN_ADMIN, 'script' => [self::answer(self::API . 'getSettings', [], [])]])->body);
}
public function testEditJobs(): void
{
$result = $this->post('edit/syncjob', ['items' => [12, '13'], 'attr' => ['active' => 0, 'mins_interval' => 60]]);
$this->assertCalled($result, self::API . 'editJobs', [[12, '13'], ['active' => 0, 'mins_interval' => 60]]);
$this->assertNotCalled($result, self::API . 'runNext');
$other = $this->post('edit/syncjob/anything', ['items' => [12], 'attr' => ['active' => 1]]);
$this->assertCalled($other, self::API . 'editJobs', [[12], ['active' => 1]]);
}
public function testRunNext(): void
{
$messages = [
self::message('danger', ['imapsync_job_running', 13], ['syncjob', 'edit', 'run_next', ['id' => [12, 13]]]),
self::message('success', ['imapsync_run_next_done', 1], ['syncjob', 'edit', 'run_next', ['id' => [12, 13]]]),
];
$result = $this->post('edit/syncjob/run_next', ['items' => [12, 13], 'attr' => ['ignored' => 1]],
['script' => [self::answer(self::API . 'runNext', [], true, $messages)]]);
$this->assertCalled($result, self::API . 'runNext', [[12, 13]]);
$this->assertNotCalled($result, self::API . 'editJobs');
$this->assertSame($messages, $result->json());
}
public function testRunNextWithoutMovedJobs(): void
{
$result = $this->post('edit/syncjob/run_next', ['items' => [99]], ['script' => [self::answer(self::API . 'runNext', [], false)]]);
$this->assertSame(['type' => 'error', 'msg' => 'Cannot edit item'], $result->json());
}
public function testEditSettings(): void
{
$result = $this->post('edit/imapsync_settings', ['items' => ['ignored'], 'attr' => ['max_parallel' => 4, 'max_kb_per_second' => 2000]]);
$this->assertCalled($result, self::API . 'editSettings', [['max_parallel' => 4, 'max_kb_per_second' => 2000]]);
}
public function testEditSources(): void
{
$attr = ['description' => 'Migration finished', 'scope' => 'user', 'users' => ['alice@example.org']];
$result = $this->post('edit/syncjob_source', ['items' => [3], 'attr' => $attr]);
$this->assertCalled($result, self::API . 'editSources', [[3], $attr]);
$this->assertNotCalled($result, self::API . 'refreshSourceToken');
}
public function testRefreshToken(): void
{
$result = $this->post('edit/syncjob_source/refresh_token', ['items' => [5, 6], 'attr' => []]);
$this->assertCalled($result, self::API . 'refreshSourceToken', [[5, 6]]);
$this->assertNotCalled($result, self::API . 'editSources');
}
public function testDeleteJobs(): void
{
$result = $this->post('delete/syncjob', [12, '13']);
$this->assertCalled($result, self::API . 'deleteJobs', [[12, '13']]);
}
public function testDeleteSources(): void
{
$messages = [self::message('danger', ['imapsync_source_in_use', 'Old provider'], ['syncjob', 'delete', 'source', ['id' => [3]]])];
$result = $this->post('delete/syncjob_source', [3], ['script' => [self::answer(self::API . 'deleteSources', [], true, $messages)]]);
$this->assertCalled($result, self::API . 'deleteSources', [[3]]);
$this->assertSame($messages, $result->json());
}
}