mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-15 12:16:41 +00:00
[Web] API: complete the OpenAPI docs, add route tests for json_api.php
This commit is contained in:
47 files changed
+18848
-7491
No files matched your search
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
|
||||
/** Routes admin: administrator accounts */
|
||||
class AdminRoutesTest extends ApiTestCase
|
||||
{
|
||||
private const DETAILS = ['username' => 'admin', 'tfa_active' => 0, 'tfa_active_int' => 0, 'active' => 1, 'active_int' => 1,
|
||||
'created' => '2026-09-21 10:23:08', 'attributes' => ['force_tfa' => '0', 'force_pw_update' => '0']];
|
||||
|
||||
public function testGetAllCollectsTheDetailsOfEveryAdmin(): void
|
||||
{
|
||||
$result = $this->get('get/admin/all', ['script' => [
|
||||
self::answer('admin', ['get'], ['admin', 'gone']),
|
||||
self::answer('admin', ['details', 'admin'], self::DETAILS),
|
||||
self::answer('admin', ['details', 'gone'], false),
|
||||
]]);
|
||||
|
||||
$this->assertSame([self::DETAILS], $result->json());
|
||||
$this->assertSame([['get'], ['details', 'admin'], ['details', 'gone']], $result->callsTo('admin'));
|
||||
}
|
||||
|
||||
public function testGetAllWithoutAdmins(): void
|
||||
{
|
||||
$result = $this->get('get/admin/all', ['script' => [self::answer('admin', ['get'], [])]]);
|
||||
|
||||
$this->assertSame('{}', $result->body);
|
||||
$this->assertSame([['get']], $result->callsTo('admin'));
|
||||
}
|
||||
|
||||
public function testGetOne(): void
|
||||
{
|
||||
$found = $this->get('get/admin/admin', ['script' => [self::answer('admin', ['details', 'admin'], self::DETAILS)]]);
|
||||
$this->assertSame(self::DETAILS, $found->json());
|
||||
|
||||
$missing = $this->get('get/admin/nobody', ['script' => [self::answer('admin', ['details'], false)]]);
|
||||
$this->assertSame('{}', $missing->body);
|
||||
$this->assertCalled($missing, 'admin', ['details', 'nobody']);
|
||||
}
|
||||
|
||||
public function testAdd(): void
|
||||
{
|
||||
$attr = ['username' => 'admin2', 'password' => 'Rand0m-Passw0rd!', 'password2' => 'Rand0m-Passw0rd!', 'active' => 1];
|
||||
$messages = [self::message('success', ['admin_added', 'admin2'], ['admin', 'add', ['username' => 'admin2', 'password' => '*']])];
|
||||
|
||||
$result = $this->post('add/admin', $attr, ['script' => [self::answer('admin', ['add'], null, $messages)]]);
|
||||
|
||||
$this->assertSame([['add', $attr]], $result->callsTo('admin'));
|
||||
$this->assertSame($messages, $result->json());
|
||||
}
|
||||
|
||||
public function testEdit(): void
|
||||
{
|
||||
$result = $this->post('edit/admin', ['items' => ['admin2', 'admin3'], 'attr' => ['active' => '0', 'force_tfa' => 1]]);
|
||||
|
||||
$this->assertSame([['edit', ['username' => ['admin2', 'admin3'], 'active' => '0', 'force_tfa' => 1]]], $result->callsTo('admin'));
|
||||
}
|
||||
|
||||
public function testEditTakesASingleUsername(): void
|
||||
{
|
||||
$result = $this->post('edit/admin', ['items' => 'admin2', 'attr' => ['password' => 'x', 'password2' => 'x']]);
|
||||
|
||||
$this->assertCalled($result, 'admin', ['edit', ['username' => ['admin2'], 'password' => 'x', 'password2' => 'x']]);
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
$messages = [
|
||||
self::message('success', ['admin_removed', 'admin2']),
|
||||
self::message('danger', ['username_invalid', 'nobody']),
|
||||
];
|
||||
|
||||
$result = $this->post('delete/admin', ['admin2', 'nobody'], ['script' => [self::answer('admin', ['delete'], null, $messages)]]);
|
||||
|
||||
$this->assertSame([['delete', ['username' => ['admin2', 'nobody']]]], $result->callsTo('admin'));
|
||||
$this->assertSame($messages, $result->json());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
|
||||
/** Routes api-key: the ApiKey module */
|
||||
class ApiKeyRoutesTest extends ApiTestCase
|
||||
{
|
||||
private const API = 'Mailcow\ApiKey\ApiKeyModule->api->';
|
||||
private const KEY = ['id' => 3, 'description' => 'provisioning', 'access' => 'rw', 'api_key' => '1B7E2C-94F0A3-6D5B18-C2E7F4-03A9D6',
|
||||
'allow_from' => ['192.0.2.10'], 'skip_ip_check' => 0, 'service' => 0, 'active' => 1, 'created' => '2026-11-02 09:12:44', 'modified' => null];
|
||||
|
||||
public function testGetAll(): void
|
||||
{
|
||||
$result = $this->get('get/api-key/all', ['script' => [self::answer(self::API . 'getKeys', [], [self::KEY])]]);
|
||||
|
||||
$this->assertSame([self::KEY], $result->json());
|
||||
$this->assertSame([[]], $result->callsTo(self::API . 'getKeys'));
|
||||
}
|
||||
|
||||
public function testGetAllAnswersAnEmptyObjectWithoutKeys(): void
|
||||
{
|
||||
// also the answer for read-only API keys: the module returns no keys for them
|
||||
$result = $this->get('get/api-key/all', ['session' => self::API_RO, 'script' => [self::answer(self::API . 'getKeys', [], [])]]);
|
||||
|
||||
$this->assertSame('{}', $result->body);
|
||||
}
|
||||
|
||||
public function testGetOne(): void
|
||||
{
|
||||
$found = $this->get('get/api-key/3', ['script' => [self::answer(self::API . 'getKey', ['3'], self::KEY)]]);
|
||||
$this->assertSame(self::KEY, $found->json());
|
||||
|
||||
$missing = $this->get('get/api-key/99', ['script' => [self::answer(self::API . 'getKey', [], [])]]);
|
||||
$this->assertSame('{}', $missing->body);
|
||||
$this->assertSame([['99']], $missing->callsTo(self::API . 'getKey'));
|
||||
}
|
||||
|
||||
public function testAdd(): void
|
||||
{
|
||||
$attr = ['description' => 'provisioning', 'access' => 'rw', 'allow_from' => '192.0.2.10, 2001:db8::/64', 'skip_ip_check' => 0, 'active' => 1];
|
||||
$messages = [self::message('success', ['api_key_added', 'provisioning'], ['api', 'add', 'key', $attr])];
|
||||
|
||||
$result = $this->post('add/api-key', $attr, ['script' => [self::answer(self::API . 'addKey', [], true, $messages)]]);
|
||||
|
||||
$this->assertSame([[$attr]], $result->callsTo(self::API . 'addKey'));
|
||||
$this->assertSame($messages, $result->json());
|
||||
}
|
||||
|
||||
public function testEdit(): void
|
||||
{
|
||||
$result = $this->post('edit/api-key', ['items' => [3, 4], 'attr' => ['regen_key' => 1]]);
|
||||
|
||||
$this->assertSame([[[3, 4], ['regen_key' => 1]]], $result->callsTo(self::API . 'editKeys'));
|
||||
}
|
||||
|
||||
public function testEditTakesASingleId(): void
|
||||
{
|
||||
$result = $this->post('edit/api-key', ['items' => 3, 'attr' => ['active' => 0]]);
|
||||
|
||||
$this->assertSame([[[3], ['active' => 0]]], $result->callsTo(self::API . 'editKeys'));
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
$messages = [self::message('danger', ['api_key_service_locked', 'API_KEY'])];
|
||||
|
||||
$result = $this->post('delete/api-key', [1], ['script' => [self::answer(self::API . 'deleteKeys', [], true, $messages)]]);
|
||||
|
||||
$this->assertSame([[[1]]], $result->callsTo(self::API . 'deleteKeys'));
|
||||
$this->assertSame($messages, $result->json());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
|
||||
/** Routes domain-admin, da-acl and sso/domain-admin */
|
||||
class DomainAdminRoutesTest extends ApiTestCase
|
||||
{
|
||||
private const DETAILS = ['username' => 'domainadmin1', 'tfa_active' => 0, 'tfa_active_int' => 0, 'active' => 1, 'active_int' => 1,
|
||||
'created' => '2026-09-21 10:24:48', 'attributes' => ['force_tfa' => '0', 'force_pw_update' => '0'],
|
||||
'selected_domains' => ['example.org'], 'unselected_domains' => ['example.com']];
|
||||
|
||||
public function testGetAllCollectsTheDetailsOfEveryDomainAdmin(): void
|
||||
{
|
||||
$result = $this->get('get/domain-admin/all', ['script' => [
|
||||
self::answer('domain_admin', ['get'], ['domainadmin1', 'gone']),
|
||||
self::answer('domain_admin', ['details', 'domainadmin1'], self::DETAILS),
|
||||
self::answer('domain_admin', ['details', 'gone'], false),
|
||||
]]);
|
||||
|
||||
$this->assertSame([self::DETAILS], $result->json());
|
||||
$this->assertSame([['get'], ['details', 'domainadmin1'], ['details', 'gone']], $result->callsTo('domain_admin'));
|
||||
}
|
||||
|
||||
public function testGetAllWithoutDomainAdmins(): void
|
||||
{
|
||||
$result = $this->get('get/domain-admin/all', ['script' => [self::answer('domain_admin', ['get'], [])]]);
|
||||
|
||||
$this->assertSame('{}', $result->body);
|
||||
}
|
||||
|
||||
public function testGetOne(): void
|
||||
{
|
||||
$found = $this->get('get/domain-admin/domainadmin1', ['script' => [self::answer('domain_admin', ['details', 'domainadmin1'], self::DETAILS)]]);
|
||||
$this->assertSame(self::DETAILS, $found->json());
|
||||
|
||||
$missing = $this->get('get/domain-admin/nobody', ['script' => [self::answer('domain_admin', ['details'], false)]]);
|
||||
$this->assertSame('{}', $missing->body);
|
||||
$this->assertSame([['details', 'nobody']], $missing->callsTo('domain_admin'));
|
||||
}
|
||||
|
||||
public function testAdd(): void
|
||||
{
|
||||
$attr = ['username' => 'domainadmin1', 'password' => 'Rand0m-Passw0rd!', 'password2' => 'Rand0m-Passw0rd!',
|
||||
'domains' => ['example.org', 'example.com'], 'active' => 1];
|
||||
|
||||
$result = $this->post('add/domain-admin', $attr);
|
||||
|
||||
$this->assertSame([['add', $attr]], $result->callsTo('domain_admin'));
|
||||
}
|
||||
|
||||
public function testEdit(): void
|
||||
{
|
||||
$result = $this->post('edit/domain-admin', ['items' => ['domainadmin1'], 'attr' => ['domains' => ['example.org'], 'disable_tfa' => 1]]);
|
||||
|
||||
$this->assertSame([['edit', ['username' => ['domainadmin1'], 'domains' => ['example.org'], 'disable_tfa' => 1]]], $result->callsTo('domain_admin'));
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
$result = $this->post('delete/domain-admin', ['domainadmin1', 'domainadmin2']);
|
||||
|
||||
$this->assertSame([['delete', ['username' => ['domainadmin1', 'domainadmin2']]]], $result->callsTo('domain_admin'));
|
||||
}
|
||||
|
||||
public function testEditAcl(): void
|
||||
{
|
||||
$messages = [self::message('success', ['acl_saved', 'domainadmin1'])];
|
||||
|
||||
$result = $this->post('edit/da-acl', ['items' => 'domainadmin1', 'attr' => ['da_acl' => ['syncjobs', 'quarantine']]],
|
||||
['script' => [self::answer('acl', ['edit', 'domainadmin'], null, $messages)]]);
|
||||
|
||||
$this->assertSame([['edit', 'domainadmin', ['username' => ['domainadmin1'], 'da_acl' => ['syncjobs', 'quarantine']]]], $result->callsTo('acl'));
|
||||
$this->assertSame($messages, $result->json());
|
||||
}
|
||||
|
||||
public function testSsoTokenIsTheAnswer(): void
|
||||
{
|
||||
$token = ['token' => '591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC'];
|
||||
|
||||
$result = $this->post('add/sso/domain-admin', ['username' => 'domainadmin1'],
|
||||
['script' => [self::answer('domain_admin_sso', ['issue'], $token)]]);
|
||||
|
||||
$this->assertSame([['issue', ['username' => 'domainadmin1']]], $result->callsTo('domain_admin_sso'));
|
||||
$this->assertSame($token, $result->json());
|
||||
}
|
||||
|
||||
public function testSsoTokenFailureAnswersTheMessages(): void
|
||||
{
|
||||
$messages = [self::message('danger', ['object_doesnt_exist', 'nobody'])];
|
||||
|
||||
$failed = $this->post('add/sso/domain-admin', ['username' => 'nobody'],
|
||||
['script' => [self::answer('domain_admin_sso', ['issue'], false, $messages)]]);
|
||||
$this->assertSame($messages, $failed->json());
|
||||
|
||||
$silent = $this->post('add/sso/domain-admin', ['username' => 'nobody'],
|
||||
['script' => [self::answer('domain_admin_sso', ['issue'], false)]]);
|
||||
$this->assertSame(['type' => 'error', 'msg' => 'Cannot add item'], $silent->json());
|
||||
}
|
||||
|
||||
public function testSsoForOtherObjectsAnswersNothing(): void
|
||||
{
|
||||
$result = $this->post('add/sso/mailbox', ['username' => 'user@example.org']);
|
||||
|
||||
$this->assertSame([200, ''], [$result->status, $result->body]);
|
||||
$this->assertNotCalled($result, 'domain_admin_sso');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
/** Routes logs/<type>[/<count>] */
|
||||
class LogRoutesTest extends ApiTestCase
|
||||
{
|
||||
/** @return array<string, array{0: string, 1: string}> route type => log name of get_logs() */
|
||||
public static function logs(): array
|
||||
{
|
||||
return [
|
||||
'dovecot' => ['dovecot', 'dovecot-mailcow'],
|
||||
'ratelimited' => ['ratelimited', 'ratelimited'],
|
||||
'netfilter' => ['netfilter', 'netfilter-mailcow'],
|
||||
'cron' => ['cron', 'cron-mailcow'],
|
||||
'postfix' => ['postfix', 'postfix-mailcow'],
|
||||
'autodiscover' => ['autodiscover', 'autodiscover-mailcow'],
|
||||
'sogo' => ['sogo', 'sogo-mailcow'],
|
||||
'ui' => ['ui', 'mailcow-ui'],
|
||||
'sasl' => ['sasl', 'sasl'],
|
||||
'watchdog' => ['watchdog', 'watchdog-mailcow'],
|
||||
'acme' => ['acme', 'acme-mailcow'],
|
||||
'api' => ['api', 'api-mailcow'],
|
||||
'rspamd-history' => ['rspamd-history', 'rspamd-history'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('logs')]
|
||||
public function testWithoutCount(string $type, string $log): void
|
||||
{
|
||||
$entries = [['time' => '1794308452', 'message' => 'first'], ['time' => '1794308451', 'message' => 'second']];
|
||||
|
||||
$result = $this->get("get/logs/$type", ['script' => [self::answer('get_logs', [$log], $entries)]]);
|
||||
|
||||
$this->assertSame([[$log]], $result->callsTo('get_logs'));
|
||||
$this->assertSame($entries, $result->json());
|
||||
}
|
||||
|
||||
#[DataProvider('logs')]
|
||||
public function testCountOrRange(string $type, string $log): void
|
||||
{
|
||||
$this->assertSame([[$log, '50']], $this->get("get/logs/$type/50")->callsTo('get_logs'));
|
||||
$this->assertSame([[$log, '2-3']], $this->get("get/logs/$type/2-3")->callsTo('get_logs'));
|
||||
}
|
||||
|
||||
#[DataProvider('logs')]
|
||||
public function testEmptyLogs(string $type, string $log): void
|
||||
{
|
||||
$this->assertSame('{}', $this->get("get/logs/$type/5", ['script' => [self::answer('get_logs', [$log], false)]])->body);
|
||||
$this->assertSame('{}', $this->get("get/logs/$type", ['script' => [self::answer('get_logs', [$log], [])]])->body);
|
||||
}
|
||||
|
||||
public function testCountKeepsOnlyDigitsAndDashes(): void
|
||||
{
|
||||
$this->assertSame([['postfix-mailcow', '12-30']], $this->get('get/logs/postfix/a12-3x0')->callsTo('get_logs'));
|
||||
}
|
||||
|
||||
public function testTrailingSlashPassesAnEmptyCount(): void
|
||||
{
|
||||
$this->assertSame([['postfix-mailcow', '']], $this->get('get/logs/postfix/')->callsTo('get_logs'));
|
||||
}
|
||||
|
||||
public function testRspamdStatsIgnoresTheCount(): void
|
||||
{
|
||||
$stats = ['version' => '3.12.1', 'scanned' => 300, 'actions' => ['reject' => 38]];
|
||||
|
||||
$result = $this->get('get/logs/rspamd-stats/10', ['script' => [self::answer('get_logs', ['rspamd-stats'], $stats)]]);
|
||||
|
||||
$this->assertSame([['rspamd-stats']], $result->callsTo('get_logs'));
|
||||
$this->assertSame($stats, $result->json());
|
||||
}
|
||||
|
||||
public function testUnknownLog(): void
|
||||
{
|
||||
$this->assertApiError($this->get('get/logs/clamd'), 404, 'route not found');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
|
||||
/** Routes oauth2-client */
|
||||
class OAuth2ClientRoutesTest extends ApiTestCase
|
||||
{
|
||||
private const CLIENT = ['id' => 1, 'client_id' => '17c76aaa88c0', 'client_secret' => '73fc668a88147e32a31ff80c',
|
||||
'redirect_uri' => 'https://app.example.org/oauth/callback', 'grant_types' => null, 'scope' => 'profile', 'user_id' => null];
|
||||
|
||||
public function testGetAllCollectsTheDetailsOfEveryClient(): void
|
||||
{
|
||||
$result = $this->get('get/oauth2-client/all', ['script' => [
|
||||
self::answer('oauth2', ['get', 'clients'], [1, 2]),
|
||||
self::answer('oauth2', ['details', 'client', 1], self::CLIENT),
|
||||
self::answer('oauth2', ['details', 'client', 2], false),
|
||||
]]);
|
||||
|
||||
$this->assertSame([self::CLIENT], $result->json());
|
||||
$this->assertSame([['get', 'clients'], ['details', 'client', 1], ['details', 'client', 2]], $result->callsTo('oauth2'));
|
||||
}
|
||||
|
||||
public function testGetAllWithoutClients(): void
|
||||
{
|
||||
$this->assertSame('{}', $this->get('get/oauth2-client/all', ['script' => [self::answer('oauth2', ['get', 'clients'], null)]])->body);
|
||||
}
|
||||
|
||||
public function testGetOne(): void
|
||||
{
|
||||
$found = $this->get('get/oauth2-client/1', ['script' => [self::answer('oauth2', ['details', 'client', '1'], self::CLIENT)]]);
|
||||
$this->assertSame(self::CLIENT, $found->json());
|
||||
|
||||
$missing = $this->get('get/oauth2-client/9', ['script' => [self::answer('oauth2', ['details'], false)]]);
|
||||
$this->assertSame('{}', $missing->body);
|
||||
$this->assertSame([['details', 'client', '9']], $missing->callsTo('oauth2'));
|
||||
}
|
||||
|
||||
public function testAdd(): void
|
||||
{
|
||||
$result = $this->post('add/oauth2-client', ['redirect_uri' => 'https://app.example.org/oauth/callback']);
|
||||
|
||||
$this->assertSame([['add', 'client', ['redirect_uri' => 'https://app.example.org/oauth/callback']]], $result->callsTo('oauth2'));
|
||||
}
|
||||
|
||||
public function testEdit(): void
|
||||
{
|
||||
$result = $this->post('edit/oauth2-client', ['items' => [1, 2], 'attr' => ['renew_secret' => 1]]);
|
||||
|
||||
$this->assertSame([['edit', 'client', ['id' => [1, 2], 'renew_secret' => 1]]], $result->callsTo('oauth2'));
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
$result = $this->post('delete/oauth2-client', [1, 2]);
|
||||
|
||||
$this->assertSame([['delete', 'client', ['id' => [1, 2]]]], $result->callsTo('oauth2'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
|
||||
/** Routes self and fido2-fn act on the account of the session; the pending password update of a login */
|
||||
class OwnAccountRoutesTest extends ApiTestCase
|
||||
{
|
||||
private const PASSWORDS = ['user_old_pass' => 'old', 'user_new_pass' => 'N3w-Passw0rd!', 'user_new_pass2' => 'N3w-Passw0rd!'];
|
||||
|
||||
public function testSelfDoesNothingForAnApiKey(): void
|
||||
{
|
||||
$result = $this->post('edit/self', ['attr' => self::PASSWORDS]);
|
||||
|
||||
$this->assertSame([200, ''], [$result->status, $result->body]);
|
||||
$this->assertNotCalled($result, 'domain_admin');
|
||||
$this->assertNotCalled($result, 'edit_user_account');
|
||||
}
|
||||
|
||||
public function testSelfOfADomainAdmin(): void
|
||||
{
|
||||
$result = $this->post('edit/self', ['attr' => self::PASSWORDS], ['session' => self::DOMAIN_ADMIN]);
|
||||
|
||||
$this->assertSame([['edit', self::PASSWORDS]], $result->callsTo('domain_admin'));
|
||||
}
|
||||
|
||||
public function testSelfOfAMailboxUser(): void
|
||||
{
|
||||
$result = $this->post('edit/self', ['attr' => self::PASSWORDS], ['session' => self::USER]);
|
||||
|
||||
$this->assertSame([[self::PASSWORDS]], $result->callsTo('edit_user_account'));
|
||||
}
|
||||
|
||||
public function testFido2FriendlyName(): void
|
||||
{
|
||||
$attr = ['fido2_cid' => hash('sha256', 'credential'), 'fido2_fn' => 'YubiKey'];
|
||||
|
||||
$result = $this->post('edit/fido2-fn', ['attr' => $attr]);
|
||||
|
||||
$this->assertSame([[['action' => 'edit_fn', 'fido2_attrs' => $attr]]], $result->callsTo('fido2'));
|
||||
}
|
||||
|
||||
public function testPendingPasswordUpdateAllowsOnlyThePasswordRoutes(): void
|
||||
{
|
||||
$session = self::ADMIN + ['pending_pw_update' => true];
|
||||
|
||||
$this->assertCalled($this->post('edit/admin', ['items' => 'admin', 'attr' => ['password' => 'x', 'password2' => 'x']], ['session' => $session]),
|
||||
'admin', ['edit', ['username' => ['admin'], 'password' => 'x', 'password2' => 'x']]);
|
||||
$this->assertCalled($this->get('get/passwordpolicy', ['session' => $session]), 'password_complexity', ['get']);
|
||||
|
||||
$denied = $this->post('edit/domain-admin', ['items' => 'domainadmin1', 'attr' => ['active' => 0]], ['session' => $session]);
|
||||
$this->assertApiError($denied, 403, 'API access denied until the pending account setup is finished');
|
||||
$this->assertNotCalled($denied, 'domain_admin');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
/** Routes passwordpolicy, reset-password-notification, the UI customization routes and cors */
|
||||
class SettingsRoutesTest extends ApiTestCase
|
||||
{
|
||||
private const POLICY = ['length' => '8', 'chars' => '1', 'special_chars' => '0', 'lowerupper' => '1', 'numbers' => '1'];
|
||||
|
||||
public function testGetPasswordPolicy(): void
|
||||
{
|
||||
foreach (['get/passwordpolicy', 'get/passwordpolicy/all'] as $route) {
|
||||
$result = $this->get($route, ['script' => [self::answer('password_complexity', ['get'], self::POLICY)]]);
|
||||
$this->assertSame([['get']], $result->callsTo('password_complexity'), $route);
|
||||
$this->assertSame(self::POLICY, $result->json(), $route);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetPasswordPolicyAsHtml(): void
|
||||
{
|
||||
$html = '<p class="help-block small">- Minimum password length is 8</p>';
|
||||
|
||||
$result = $this->get('get/passwordpolicy/html', ['script' => [self::answer('password_complexity', ['html'], $html)]]);
|
||||
|
||||
$this->assertSame([['html']], $result->callsTo('password_complexity'));
|
||||
$this->assertSame($html, $result->json());
|
||||
}
|
||||
|
||||
public function testGetPasswordPolicyFailure(): void
|
||||
{
|
||||
$this->assertSame('{}', $this->get('get/passwordpolicy', ['script' => [self::answer('password_complexity', ['get'], false)]])->body);
|
||||
$this->assertSame('{}', $this->get('get/passwordpolicy/html', ['script' => [self::answer('password_complexity', ['html'], false)]])->body);
|
||||
}
|
||||
|
||||
public function testEditPasswordPolicyIgnoresItems(): void
|
||||
{
|
||||
$result = $this->post('edit/passwordpolicy', ['items' => ['x'], 'attr' => ['length' => 12, 'numbers' => 1]]);
|
||||
|
||||
$this->assertSame([['edit', ['length' => 12, 'numbers' => 1]]], $result->callsTo('password_complexity'));
|
||||
}
|
||||
|
||||
public function testEditPasswordResetNotification(): void
|
||||
{
|
||||
$attr = ['from' => 'noreply@example.org', 'subject' => 'Reset your password', 'text_tmpl' => '{{link}}', 'html_tmpl' => ''];
|
||||
|
||||
$result = $this->post('edit/reset-password-notification', ['attr' => $attr]);
|
||||
|
||||
$this->assertSame([['edit_notification', $attr]], $result->callsTo('reset_password'));
|
||||
}
|
||||
|
||||
/** @return array<string, array{0: string, 1: array}> */
|
||||
public static function customizations(): array
|
||||
{
|
||||
return [
|
||||
'ui_texts' => ['ui_texts', ['title_name' => 'Example Mail', 'ui_announcement_type' => 'warning', 'ui_announcement_active' => 1]],
|
||||
'ip_check' => ['ip_check', ['ip_check_opt_in' => '1']],
|
||||
'custom_login' => ['custom_login', ['hide_admin_quicklink' => 1, 'force_sso' => 0]],
|
||||
'ui_theme' => ['ui_theme', ['ui_theme' => 'servercow']],
|
||||
'app_links' => ['app_links', ['app' => ['Wiki'], 'href' => ['https://wiki.example.org'], 'user_href' => [''], 'hide' => ['0']]],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('customizations')]
|
||||
public function testCustomizationsPassAttrOnly(string $item, array $attr): void
|
||||
{
|
||||
$messages = [self::message('success', $item === 'ip_check' ? 'ip_check_opt_in_modified' : $item)];
|
||||
|
||||
$result = $this->post("edit/$item", ['items' => ['ignored'], 'attr' => $attr], ['script' => [self::answer('customize', ['edit', $item], null, $messages)]]);
|
||||
|
||||
$this->assertSame([['edit', $item, $attr]], $result->callsTo('customize'));
|
||||
$this->assertSame($messages, $result->json());
|
||||
}
|
||||
|
||||
public function testCustomizationWithoutAttrPassesNothing(): void
|
||||
{
|
||||
$result = $this->post('edit/app_links', ['items' => []]);
|
||||
|
||||
$this->assertSame([['edit', 'app_links', []]], $result->callsTo('customize'));
|
||||
}
|
||||
|
||||
public function testEditCors(): void
|
||||
{
|
||||
$attr = ['allowed_origins' => ['https://app.example.org'], 'allowed_methods' => ['GET', 'POST']];
|
||||
|
||||
$result = $this->post('edit/cors', ['attr' => $attr]);
|
||||
|
||||
$this->assertSame([['set_headers'], ['edit', $attr]], $result->callsTo('cors'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Tests\Api\Admin;
|
||||
|
||||
use Mailcow\Tests\Api\ApiTestCase;
|
||||
|
||||
/** Routes status/<object> */
|
||||
class StatusRoutesTest extends ApiTestCase
|
||||
{
|
||||
public function testContainersAreReducedToStateStartAndImage(): void
|
||||
{
|
||||
$info = [
|
||||
'dovecot-mailcow' => ['State' => ['Status' => 'running', 'StartedAt' => '2026-11-10T11:00:51.541059812Z', 'Pid' => 42],
|
||||
'Config' => ['Image' => 'ghcr.io/mailcow/dovecot:2.3.21.1-4', 'Hostname' => 'dovecot'], 'Id' => '5d0f3cbb2a6e'],
|
||||
'ofelia-mailcow' => ['State' => ['Status' => 'exited', 'StartedAt' => '2026-11-09T08:00:00Z'],
|
||||
'Config' => ['Image' => 'mcuadros/ofelia:latest'], 'Id' => '77aa01'],
|
||||
];
|
||||
|
||||
$result = $this->get('get/status/containers', ['script' => [self::answer('docker', ['info'], $info)]]);
|
||||
|
||||
$this->assertSame([['info']], $result->callsTo('docker'));
|
||||
$this->assertSame([
|
||||
'dovecot-mailcow' => ['type' => 'info', 'container' => 'dovecot-mailcow', 'state' => 'running',
|
||||
'started_at' => '2026-11-10T11:00:51.541059812Z', 'image' => 'ghcr.io/mailcow/dovecot:2.3.21.1-4'],
|
||||
'ofelia-mailcow' => ['type' => 'info', 'container' => 'ofelia-mailcow', 'state' => 'exited',
|
||||
'started_at' => '2026-11-09T08:00:00Z', 'image' => 'mcuadros/ofelia:latest'],
|
||||
], $result->json());
|
||||
$this->assertStringContainsString('"ghcr.io/mailcow/dovecot', $result->body);
|
||||
}
|
||||
|
||||
public function testContainersWithoutDockerapi(): void
|
||||
{
|
||||
$result = $this->get('get/status/containers', ['script' => [self::answer('docker', ['info'], 'Could not resolve host: dockerapi')]]);
|
||||
|
||||
$this->assertSame('null', $result->body);
|
||||
}
|
||||
|
||||
public function testContainerStatsByContainerId(): void
|
||||
{
|
||||
$stats = [['read' => '2026-11-10T19:20:01Z', 'pids_stats' => ['current' => 38]], ['read' => '2026-11-10T19:20:06Z', 'pids_stats' => ['current' => 39]]];
|
||||
|
||||
$result = $this->get('get/status/container/5d0f3cbb2a6e', ['script' => [self::answer('docker', ['container_stats'], $stats)]]);
|
||||
|
||||
$this->assertSame([['container_stats', '5d0f3cbb2a6e']], $result->callsTo('docker'));
|
||||
$this->assertSame($stats, $result->json());
|
||||
}
|
||||
|
||||
public function testContainerStatsWithoutId(): void
|
||||
{
|
||||
$result = $this->get('get/status/container', ['script' => [self::answer('docker', ['container_stats'], false)]]);
|
||||
|
||||
$this->assertSame([['container_stats', null]], $result->callsTo('docker'));
|
||||
$this->assertSame('false', $result->body);
|
||||
}
|
||||
|
||||
public function testVmailSplitsTheDfLine(): void
|
||||
{
|
||||
$df = json_encode('/dev/sda1,105G,65G,40G,66%,/var/vmail');
|
||||
|
||||
$result = $this->get('get/status/vmail', ['script' => [self::answer('docker', ['post', 'dovecot-mailcow', 'exec'], $df)]]);
|
||||
|
||||
$this->assertSame([['post', 'dovecot-mailcow', 'exec', ['cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail']]], $result->callsTo('docker'));
|
||||
$this->assertSame(['type' => 'info', 'disk' => '/dev/sda1', 'used' => '65G', 'total' => '105G', 'used_percent' => '66%'], $result->json());
|
||||
}
|
||||
|
||||
public function testHost(): void
|
||||
{
|
||||
$stats = ['cpu' => ['cores' => 4, 'usage' => 6], 'memory' => ['total' => 12541669376, 'usage' => 62.5], 'uptime' => 703800.5,
|
||||
'system_time' => '10.11.2026 19:13:07', 'architecture' => 'x86_64'];
|
||||
|
||||
$result = $this->get('get/status/host', ['script' => [self::answer('docker', ['host_stats'], $stats)]]);
|
||||
|
||||
$this->assertSame([['host_stats']], $result->callsTo('docker'));
|
||||
$this->assertSame($stats, $result->json());
|
||||
}
|
||||
|
||||
public function testPublicAddressesAskMailcowEmail(): void
|
||||
{
|
||||
$result = $this->get('get/status/host/ip', ['script' => [
|
||||
self::answer('curl_exec', [], '198.51.100.7', once: true),
|
||||
self::answer('curl_exec', [], '2001:db8::7', once: true),
|
||||
]]);
|
||||
|
||||
$this->assertSame([[null, CURLOPT_URL, 'http://ipv4.mailcow.email'], [null, CURLOPT_URL, 'http://ipv6.mailcow.email']],
|
||||
array_values(array_filter($result->callsTo('curl_setopt'), fn(array $args): bool => $args[1] === CURLOPT_URL)));
|
||||
$this->assertSame(['ipv4' => '198.51.100.7', 'ipv6' => '2001:db8::7'], $result->json());
|
||||
}
|
||||
|
||||
public function testHostWithAnotherSegmentAnswersNothing(): void
|
||||
{
|
||||
$result = $this->get('get/status/host/cpu');
|
||||
|
||||
$this->assertSame([200, ''], [$result->status, $result->body]);
|
||||
$this->assertNotCalled($result, 'docker');
|
||||
}
|
||||
|
||||
public function testVersion(): void
|
||||
{
|
||||
// $MAILCOW_GIT_VERSION comes from app_info.inc.php, which the harness does not load
|
||||
$this->assertSame(['version' => null], $this->get('get/status/version')->json());
|
||||
}
|
||||
|
||||
public function testUnknownStatusAnswersNothing(): void
|
||||
{
|
||||
$result = $this->get('get/status/disks');
|
||||
|
||||
$this->assertSame([200, ''], [$result->status, $result->body]);
|
||||
}
|
||||
|
||||
public function testOnlyForAdmins(): void
|
||||
{
|
||||
foreach (['containers', 'vmail', 'host', 'version'] as $object) {
|
||||
$result = $this->get("get/status/$object", ['session' => self::DOMAIN_ADMIN]);
|
||||
$this->assertSame([200, ''], [$result->status, $result->body], $object);
|
||||
$this->assertNotCalled($result, 'docker');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user