Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F262115
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
121 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php
index c51b636a..76374a38 100644
--- a/src/app/Http/Controllers/RelationController.php
+++ b/src/app/Http/Controllers/RelationController.php
@@ -1,404 +1,419 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
class RelationController extends ResourceController
{
/** @var array Common object properties in the API response */
protected $objectProps = [];
/** @var string Resource localization label */
protected $label = '';
/** @var string Resource model name */
protected $model = '';
/** @var array Resource listing order (column names) */
protected $order = [];
/** @var array Resource relation method arguments */
protected $relationArgs = [];
/**
* Delete a resource.
*
* @param string $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
$resource = $this->model::find($id);
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canDelete($resource)) {
return $this->errorResponse(403);
}
$resource->delete();
return response()->json([
'status' => 'success',
'message' => \trans("app.{$this->label}-delete-success"),
]);
}
/**
* Listing of resources belonging to the authenticated user.
*
* The resource entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$method = Str::plural(\lcfirst(\class_basename($this->model)));
$query = call_user_func_array([$user, $method], $this->relationArgs);
if (!empty($this->order)) {
foreach ($this->order as $col) {
$query->orderBy($col);
}
}
// TODO: Search and paging
$result = $query->get()
->map(function ($resource) {
return $this->objectToClient($resource);
});
$result = [
'list' => $result,
'count' => count($result),
'hasMore' => false,
'message' => \trans("app.search-foundx{$this->label}s", ['x' => count($result)]),
];
return response()->json($result);
}
/**
* Prepare resource statuses for the UI
*
* @param object $resource Resource object
*
* @return array Statuses array
*/
protected static function objectState($resource): array
{
$state = [];
$reflect = new \ReflectionClass(get_class($resource));
foreach (array_keys($reflect->getConstants()) as $const) {
if (strpos($const, 'STATUS_') === 0 && $const != 'STATUS_NEW') {
$method = Str::camel('is_' . strtolower(substr($const, 7)));
$state[$method] = $resource->{$method}();
}
}
+ $with_imap = \config('app.with_imap');
+ $with_ldap = \config('app.with_ldap');
+
+ $state['isReady'] = (!$with_imap || !isset($state['isImapReady']) || $state['isImapReady'])
+ && (!$with_ldap || !isset($state['isLdapReady']) || $state['isLdapReady'])
+ && (!isset($state['isVerified']) || $state['isVerified'])
+ && (!isset($state['isConfirmed']) || $state['isConfirmed']);
+
+ if (!$with_imap) {
+ unset($state['isImapReady']);
+ }
+ if (!$with_ldap) {
+ unset($state['isLdapReady']);
+ }
+
if (empty($state['isDeleted']) && method_exists($resource, 'trashed')) {
$state['isDeleted'] = $resource->trashed();
}
return $state;
}
/**
* Prepare a resource object for the UI.
*
* @param object $object An object
* @param bool $full Include all object properties
*
* @return array Object information
*/
protected function objectToClient($object, bool $full = false): array
{
if ($full) {
$result = $object->toArray();
unset($result['tenant_id']);
} else {
$result = ['id' => $object->id];
foreach ($this->objectProps as $prop) {
$result[$prop] = $object->{$prop};
}
}
$result = array_merge($result, $this->objectState($object));
return $result;
}
/**
* Object status' process information.
*
* @param object $object The object to process
* @param array $steps The steps definition
*
* @return array Process state information
*/
protected static function processStateInfo($object, array $steps): array
{
$process = [];
$withLdap = \config('app.with_ldap');
$withImap = \config('app.with_imap');
// Create a process check list
foreach ($steps as $step_name => $state) {
// Remove LDAP related steps if the backend is disabled
if (!$withLdap && strpos($step_name, '-ldap-')) {
continue;
}
// Remove IMAP related steps if the backend is disabled
if (!$withImap && strpos($step_name, '-imap-')) {
continue;
}
$step = [
'label' => $step_name,
'title' => \trans("app.process-{$step_name}"),
];
if (is_array($state)) {
$step['link'] = $state[1];
$state = $state[0];
}
$step['state'] = $state;
$process[] = $step;
}
// Add domain specific steps
if (method_exists($object, 'domain')) {
$domain = $object->domain();
// If that is not a public domain
if ($domain && !$domain->isPublic()) {
$domain_status = API\V4\DomainsController::statusInfo($domain);
$process = array_merge($process, $domain_status['process']);
}
}
$all = count($process);
$checked = count(array_filter($process, function ($v) {
return $v['state'];
}));
$state = $all === $checked ? 'done' : 'running';
// After 180 seconds assume the process is in failed state,
// this should unlock the Refresh button in the UI
if ($all !== $checked && $object->created_at->diffInSeconds(\Carbon\Carbon::now()) > 180) {
$state = 'failed';
}
return [
'process' => $process,
'processState' => $state,
'isReady' => $all === $checked,
];
}
/**
* Object status' process information update.
*
* @param object $object The object to process
*
* @return array Process state information
*/
protected function processStateUpdate($object): array
{
$response = $this->statusInfo($object);
if (!empty(request()->input('refresh'))) {
$updated = false;
$async = false;
$last_step = 'none';
foreach ($response['process'] as $idx => $step) {
$last_step = $step['label'];
if (!$step['state']) {
$exec = $this->execProcessStep($object, $step['label']); // @phpstan-ignore-line
if (!$exec) {
if ($exec === null) {
$async = true;
}
break;
}
$updated = true;
}
}
if ($updated) {
$response = $this->statusInfo($object);
}
$success = $response['isReady'];
$suffix = $success ? 'success' : 'error-' . $last_step;
$response['status'] = $success ? 'success' : 'error';
$response['message'] = \trans('app.process-' . $suffix);
if ($async && !$success) {
$response['processState'] = 'waiting';
$response['status'] = 'success';
$response['message'] = \trans('app.process-async');
}
}
return $response;
}
/**
* Set the resource configuration.
*
* @param int $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function setConfig($id)
{
$resource = $this->model::find($id);
if (!method_exists($this->model, 'setConfig')) {
return $this->errorResponse(404);
}
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($resource)) {
return $this->errorResponse(403);
}
$errors = $resource->setConfig(request()->input());
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
return response()->json([
'status' => 'success',
'message' => \trans("app.{$this->label}-setconfig-success"),
]);
}
/**
* Display information of a resource specified by $id.
*
* @param string $id The resource to show information for.
*
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$resource = $this->model::find($id);
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($resource)) {
return $this->errorResponse(403);
}
$response = $this->objectToClient($resource, true);
if (!empty($statusInfo = $this->statusInfo($resource))) {
$response['statusInfo'] = $statusInfo;
}
// Resource configuration, e.g. sender_policy, invitation_policy, acl
if (method_exists($resource, 'getConfig')) {
$response['config'] = $resource->getConfig();
}
if (method_exists($resource, 'aliases')) {
$response['aliases'] = $resource->aliases()->pluck('alias')->all();
}
// Entitlements/Wallet info
if (method_exists($resource, 'wallet')) {
API\V4\SkusController::objectEntitlements($resource, $response);
}
return response()->json($response);
}
/**
* Get a list of SKUs available to the resource.
*
* @param int $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function skus($id)
{
$resource = $this->model::find($id);
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($resource)) {
return $this->errorResponse(403);
}
return API\V4\SkusController::objectSkus($resource);
}
/**
* Fetch resource status (and reload setup process)
*
* @param int $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function status($id)
{
$resource = $this->model::find($id);
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($resource)) {
return $this->errorResponse(403);
}
$response = $this->processStateUpdate($resource);
$response = array_merge($response, $this->objectState($resource));
return response()->json($response);
}
/**
* Resource status (extended) information
*
* @param object $resource Resource object
*
* @return array Status information
*/
public static function statusInfo($resource): array
{
return [];
}
}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index 73aca19d..530e8049 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,490 +1,490 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap')
import AppComponent from '../vue/App'
import MenuComponent from '../vue/Widgets/Menu'
import SupportForm from '../vue/Widgets/SupportForm'
import { loadLangAsync, i18n } from './locale'
import { clearFormValidation, pick, startLoading, stopLoading } from './utils'
const routerState = {
afterLogin: null,
isLoggedIn: !!localStorage.getItem('token')
}
let loadingRoute
// Note: This has to be before the app is created
// Note: You cannot use app inside of the function
window.router.beforeEach((to, from, next) => {
// check if the route requires authentication and user is not logged in
if (to.meta.requiresAuth && !routerState.isLoggedIn) {
// remember the original request, to use after login
routerState.afterLogin = to;
// redirect to login page
next({ name: 'login' })
return
}
if (to.meta.loading) {
startLoading()
loadingRoute = to.name
}
next()
})
window.router.afterEach((to, from) => {
if (to.name && loadingRoute === to.name) {
stopLoading()
loadingRoute = null
}
// When changing a page remove old:
// - error page
// - modal backdrop
$('#error-page,.modal-backdrop.show').remove()
$('body').css('padding', 0) // remove padding added by unclosed modal
// Close the mobile menu
if ($('#header-menu .navbar-collapse.show').length) {
$('#header-menu .navbar-toggler').click();
}
})
const app = new Vue({
components: {
AppComponent,
MenuComponent,
},
i18n,
router: window.router,
data() {
return {
authInfo: null,
isUser: !window.isAdmin && !window.isReseller,
appName: window.config['app.name'],
appUrl: window.config['app.url'],
themeDir: '/themes/' + window.config['app.theme']
}
},
methods: {
clearFormValidation,
countriesText(list) {
if (list && list.length) {
let result = []
list.forEach(code => {
let country = window.config.countries[code]
if (country) {
result.push(country[1])
} else {
console.warn(`Unknown country code: ${code}`)
}
})
return result.join(', ')
}
return this.$t('form.norestrictions')
},
hasPermission(type) {
const key = 'enable' + type.charAt(0).toUpperCase() + type.slice(1)
return !!(this.authInfo && this.authInfo.statusInfo[key])
},
hasRoute(name) {
return this.$router.resolve({ name: name }).resolved.matched.length > 0
},
hasSKU(name) {
return this.authInfo.statusInfo.skus && this.authInfo.statusInfo.skus.indexOf(name) != -1
},
isController(wallet_id) {
if (wallet_id && this.authInfo) {
let i
for (i = 0; i < this.authInfo.wallets.length; i++) {
if (wallet_id == this.authInfo.wallets[i].id) {
return true
}
}
for (i = 0; i < this.authInfo.accounts.length; i++) {
if (wallet_id == this.authInfo.accounts[i].id) {
return true
}
}
}
return false
},
isDegraded() {
return this.authInfo && this.authInfo.isAccountDegraded
},
// Set user state to "logged in"
loginUser(response, dashboard, update) {
if (!update) {
routerState.isLoggedIn = true
this.authInfo = null
}
localStorage.setItem('token', response.access_token)
localStorage.setItem('refreshToken', response.refresh_token)
if (response.email) {
this.authInfo = response
}
if (dashboard !== false) {
this.$router.push(routerState.afterLogin || { name: 'dashboard' })
}
routerState.afterLogin = null
// Refresh the token before it expires
let timeout = response.expires_in || 0
// We'll refresh 60 seconds before the token expires
if (timeout > 60) {
timeout -= 60
}
// TODO: We probably should try a few times in case of an error
// TODO: We probably should prevent axios from doing any requests
// while the token is being refreshed
this.refreshTimeout = setTimeout(() => {
axios.post('api/auth/refresh', { refresh_token: localStorage.getItem('refreshToken') }).then(response => {
this.loginUser(response.data, false, true)
})
}, timeout * 1000)
},
// Set user state to "not logged in"
logoutUser(redirect) {
routerState.isLoggedIn = true
this.authInfo = null
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
if (redirect !== false) {
this.$router.push({ name: 'login' })
}
clearTimeout(this.refreshTimeout)
},
logo(mode) {
let src = this.appUrl + this.themeDir + '/images/logo_' + (mode || 'header') + '.png'
return `<img src="${src}" alt="${this.appName}">`
},
pick,
startLoading,
stopLoading,
errorPage(code, msg, hint) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
// we can't really use router to display error page as it has two side
// effects: it changes the URL and adds the error page to browser history.
// For now we'll be replacing current view with error page "manually".
if (!msg) msg = this.$te('error.' + code) ? this.$t('error.' + code) : this.$t('error.unknown')
if (!hint) hint = ''
const error_page = '<div id="error-page" class="error-page">'
+ `<div class="code">${code}</div><div class="message">${msg}</div><div class="hint">${hint}</div>`
+ '</div>'
$('#error-page').remove()
$('#app').append(error_page)
app.updateBodyClass('error')
},
errorHandler(error) {
stopLoading()
const status = error.response ? error.response.status : 500
const message = error.response ? error.response.statusText : ''
if (status == 401) {
// Remember requested route to come back to it after log in
if (this.$route.meta.requiresAuth) {
routerState.afterLogin = this.$route
this.logoutUser()
} else {
this.logoutUser(false)
}
} else {
if (!error.response) {
console.error(error)
}
this.errorPage(status, message)
}
},
price(price, currency) {
if (!currency) {
currency = 'CHF'
} else {
currency = currency.toUpperCase()
}
let args = { style: 'currency', currency }
if (currency == 'BTC') {
args.minimumFractionDigits = 6
args.maximumFractionDigits = 9
}
// TODO: Set locale argument according to the currently used locale
return ((price || 0) / 100).toLocaleString('de-DE', args)
},
priceLabel(cost, discount, currency) {
let index = ''
if (discount) {
cost = Math.floor(cost * ((100 - discount) / 100))
index = '\u00B9'
}
return this.price(cost, currency) + '/' + this.$t('wallet.month') + index
},
clickRecord(event) {
if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
$(event.target).closest('tr').find('a').trigger('click')
}
},
pageName(path) {
let page = this.$route.path
// check if it is a "menu page", find the page name
// otherwise we'll use the real path as page name
window.config.menu.every(item => {
if (item.location == page && item.page) {
page = item.page
return false
}
})
page = page.replace(/^\//, '')
return page ? page : '404'
},
supportDialog(container) {
let dialog = $('#support-dialog')[0]
if (!dialog) {
// FIXME: Find a nicer way of doing this
SupportForm.i18n = i18n
let form = new Vue(SupportForm)
form.$mount($('<div>').appendTo(container)[0])
form.$root = this
form.$toast = this.$toast
dialog = form.$el
}
dialog.__vue__.show()
},
statusClass(obj) {
if (obj.isDeleted) {
return 'text-muted'
}
if (obj.isDegraded || obj.isAccountDegraded || obj.isSuspended) {
return 'text-warning'
}
- if (obj.isImapReady === false || obj.isLdapReady === false || obj.isVerified === false || obj.isConfirmed === false) {
+ if (!obj.isReady) {
return 'text-danger'
}
return 'text-success'
},
statusText(obj) {
if (obj.isDeleted) {
return this.$t('status.deleted')
}
if (obj.isDegraded || obj.isAccountDegraded) {
return this.$t('status.degraded')
}
if (obj.isSuspended) {
return this.$t('status.suspended')
}
- if (obj.isImapReady === false || obj.isLdapReady === false || obj.isVerified === false || obj.isConfirmed === false) {
+ if (!obj.isReady) {
return this.$t('status.notready')
}
return this.$t('status.active')
},
// Append some wallet properties to the object
userWalletProps(object) {
let wallet = this.authInfo.accounts[0]
if (!wallet) {
wallet = this.authInfo.wallets[0]
}
if (wallet) {
object.currency = wallet.currency
if (wallet.discount) {
object.discount = wallet.discount
object.discount_description = wallet.discount_description
}
}
},
updateBodyClass(name) {
// Add 'class' attribute to the body, different for each page
// so, we can apply page-specific styles
document.body.className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '')
}
}
})
// Fetch the locale file and the start the app
loadLangAsync().then(() => app.$mount('#app'))
// Add a axios request interceptor
axios.interceptors.request.use(
config => {
// This is the only way I found to change configuration options
// on a running application. We need this for browser testing.
config.headers['X-Test-Payment-Provider'] = window.config.paymentProvider
// Set the Authorization header. Note that some request might force
// empty Authorization header therefore we check if the header is already set,
// not whether it's empty
const token = localStorage.getItem('token')
if (token && !('Authorization' in config.headers)) {
config.headers.Authorization = 'Bearer ' + token
}
let loader = config.loader
if (loader) {
startLoading(loader)
}
return config
},
error => {
// Do something with request error
return Promise.reject(error)
}
)
// Add a axios response interceptor for general/validation error handler
axios.interceptors.response.use(
response => {
if (response.config.onFinish) {
response.config.onFinish()
}
let loader = response.config.loader
if (loader) {
stopLoading(loader)
}
return response
},
error => {
if (error.config && error.config.loader) {
stopLoading(error.config.loader)
}
// Do not display the error in a toast message, pass the error as-is
if (axios.isCancel(error) || (error.config && error.config.ignoreErrors)) {
return Promise.reject(error)
}
if (error.config && error.config.onFinish) {
error.config.onFinish()
}
let error_msg
const status = error.response ? error.response.status : 200
const data = error.response ? error.response.data : {}
if (status == 422 && data.errors) {
error_msg = app.$t('error.form')
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
$.each(data.errors, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
if (!input.length) {
input = form.find('[name="' + input_name + '"]');
}
if (input.length) {
// Create an error message
// API responses can use a string, array or object
let msg_text = ''
if (typeof(msg) !== 'string') {
$.each(msg, (index, str) => {
msg_text += str + ' '
})
}
else {
msg_text = msg
}
let feedback = $('<div class="invalid-feedback">').text(msg_text)
if (input.is('.list-input')) {
// List input widget
let controls = input.children(':not(:first-child)')
if (!controls.length && typeof msg == 'string') {
// this is an empty list (the main input only)
// and the error message is not an array
input.find('.main-input').addClass('is-invalid')
} else {
controls.each((index, element) => {
if (msg[index]) {
$(element).find('input').addClass('is-invalid')
}
})
}
input.addClass('is-invalid').next('.invalid-feedback').remove()
input.after(feedback)
} else {
// a special case, e.g. the invitation policy widget
if (input.is('select') && input.parent().is('.input-group-select.selected')) {
input = input.next()
}
// Standard form element
input.addClass('is-invalid')
input.parent().find('.invalid-feedback').remove()
input.parent().append(feedback)
}
}
})
form.find('.is-invalid:not(.list-input)').first().focus()
})
}
else if (data.status == 'error') {
error_msg = data.message
}
else {
error_msg = error.request ? error.request.statusText : error.message
}
app.$toast.error(error_msg || app.$t('error.server'))
// Pass the error as-is
return Promise.reject(error)
}
)
diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php
index 1b419ae1..1e417fb6 100644
--- a/src/tests/Feature/Controller/DomainsTest.php
+++ b/src/tests/Feature/Controller/DomainsTest.php
@@ -1,608 +1,618 @@
<?php
namespace Tests\Feature\Controller;
use App\Domain;
use App\Entitlement;
use App\Sku;
use App\Tenant;
use App\User;
use App\Wallet;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
class DomainsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('test1@' . \config('app.domain'));
$this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
Sku::where('title', 'test')->delete();
}
public function tearDown(): void
{
$this->deleteTestUser('test1@' . \config('app.domain'));
$this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
Sku::where('title', 'test')->delete();
$domain = $this->getTestDomain('kolab.org');
$domain->settings()->whereIn('key', ['spf_whitelist'])->delete();
parent::tearDown();
}
/**
* Test domain confirm request
* @group skipci
*/
public function testConfirm(): void
{
Queue::fake();
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$user = $this->getTestUser('test1@domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
Entitlement::create([
'wallet_id' => $user->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('error', $json['status']);
$this->assertEquals('Domain ownership verification failed.', $json['message']);
$domain->status |= Domain::STATUS_CONFIRMED;
$domain->save();
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals('success', $json['status']);
$this->assertEquals('Domain verified successfully.', $json['message']);
$this->assertTrue(is_array($json['statusInfo']));
// Not authorized access
$response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm");
$response->assertStatus(403);
// Authorized access by additional account controller
$domain = $this->getTestDomain('kolab.org');
$response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}/confirm");
$response->assertStatus(200);
}
/**
* Test domain delete request (DELETE /api/v4/domains/<id>)
*/
public function testDestroy(): void
{
Queue::fake();
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$john = $this->getTestUser('john@kolab.org');
$johns_domain = $this->getTestDomain('kolab.org');
$user1 = $this->getTestUser('test1@' . \config('app.domain'));
$user2 = $this->getTestUser('test2@' . \config('app.domain'));
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
Entitlement::create([
'wallet_id' => $user1->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
// Not authorized access
$response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}");
$response->assertStatus(403);
// Can't delete non-empty domain
$response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}");
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('error', $json['status']);
$this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']);
// Successful deletion
$response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('success', $json['status']);
$this->assertEquals('Domain deleted successfully.', $json['message']);
$this->assertTrue($domain->fresh()->trashed());
// Authorized access by additional account controller
$this->deleteTestDomain('domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
Entitlement::create([
'wallet_id' => $user1->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
$user1->wallets()->first()->addController($user2);
$response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('success', $json['status']);
$this->assertEquals('Domain deleted successfully.', $json['message']);
$this->assertTrue($domain->fresh()->trashed());
}
/**
* Test fetching domains list
*/
public function testIndex(): void
{
// User with no domains
$user = $this->getTestUser('test1@domainscontroller.com');
$response = $this->actingAs($user)->get("api/v4/domains");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(4, $json);
$this->assertSame(0, $json['count']);
$this->assertSame(false, $json['hasMore']);
$this->assertSame("0 domains have been found.", $json['message']);
$this->assertSame([], $json['list']);
// User with custom domain(s)
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$response = $this->actingAs($john)->get("api/v4/domains");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(4, $json);
$this->assertSame(1, $json['count']);
$this->assertSame(false, $json['hasMore']);
$this->assertSame("1 domains have been found.", $json['message']);
$this->assertCount(1, $json['list']);
$this->assertSame('kolab.org', $json['list'][0]['namespace']);
// Values below are tested by Unit tests
$this->assertArrayHasKey('isConfirmed', $json['list'][0]);
$this->assertArrayHasKey('isDeleted', $json['list'][0]);
$this->assertArrayHasKey('isVerified', $json['list'][0]);
$this->assertArrayHasKey('isSuspended', $json['list'][0]);
$this->assertArrayHasKey('isActive', $json['list'][0]);
- $this->assertArrayHasKey('isLdapReady', $json['list'][0]);
+ if (\config('app.with_ldap')) {
+ $this->assertArrayHasKey('isLdapReady', $json['list'][0]);
+ } else {
+ $this->assertArrayNotHasKey('isLdapReady', $json['list'][0]);
+ }
+ $this->assertArrayHasKey('isReady', $json['list'][0]);
$response = $this->actingAs($ned)->get("api/v4/domains");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(4, $json);
$this->assertCount(1, $json['list']);
$this->assertSame('kolab.org', $json['list'][0]['namespace']);
}
/**
* Test domain config update (POST /api/v4/domains/<domain>/config)
*/
public function testSetConfig(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$domain = $this->getTestDomain('kolab.org');
$domain->setSetting('spf_whitelist', null);
// Test unknown domain id
$post = ['spf_whitelist' => []];
$response = $this->actingAs($john)->post("/api/v4/domains/123/config", $post);
$json = $response->json();
$response->assertStatus(404);
// Test access by user not being a wallet controller
$post = ['spf_whitelist' => []];
$response = $this->actingAs($jack)->post("/api/v4/domains/{$domain->id}/config", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['grey' => 1];
$response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertSame('The requested configuration parameter is not supported.', $json['errors']['grey']);
$this->assertNull($domain->fresh()->getSetting('spf_whitelist'));
// Test some valid data
$post = ['spf_whitelist' => ['.test.domain.com']];
$response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('Domain settings updated successfully.', $json['message']);
$expected = \json_encode($post['spf_whitelist']);
$this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist'));
// Test input validation
$post = ['spf_whitelist' => ['aaa']];
$response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame(
'The entry format is invalid. Expected a domain name starting with a dot.',
$json['errors']['spf_whitelist'][0]
);
$this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist'));
}
/**
* Test fetching domain info
*/
public function testShow(): void
{
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$user = $this->getTestUser('test1@domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
$discount = \App\Discount::withEnvTenantContext()->where('code', 'TEST')->first();
$wallet = $user->wallet();
$wallet->discount()->associate($discount);
$wallet->save();
Entitlement::create([
'wallet_id' => $user->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals($domain->id, $json['id']);
$this->assertEquals($domain->namespace, $json['namespace']);
$this->assertEquals($domain->status, $json['status']);
$this->assertEquals($domain->type, $json['type']);
$this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']);
$this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']);
$this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']);
$this->assertSame([], $json['config']['spf_whitelist']);
$this->assertCount(4, $json['mx']);
$this->assertTrue(strpos(implode("\n", $json['mx']), $domain->namespace) !== false);
$this->assertCount(8, $json['dns']);
$this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false);
$this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false);
$this->assertTrue(is_array($json['statusInfo']));
// Values below are tested by Unit tests
$this->assertArrayHasKey('isConfirmed', $json);
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isVerified', $json);
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
- $this->assertArrayHasKey('isLdapReady', $json);
+ if (\config('app.with_ldap')) {
+ $this->assertArrayHasKey('isLdapReady', $json);
+ } else {
+ $this->assertArrayNotHasKey('isLdapReady', $json);
+ }
+ $this->assertArrayHasKey('isReady', $json);
$this->assertCount(1, $json['skus']);
$this->assertSame(1, $json['skus'][$sku_domain->id]['count']);
$this->assertSame([0], $json['skus'][$sku_domain->id]['costs']);
$this->assertSame($wallet->id, $json['wallet']['id']);
$this->assertSame($wallet->balance, $json['wallet']['balance']);
$this->assertSame($wallet->currency, $json['wallet']['currency']);
$this->assertSame($discount->discount, $json['wallet']['discount']);
$this->assertSame($discount->description, $json['wallet']['discount_description']);
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
// Not authorized - Other account domain
$response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}");
$response->assertStatus(403);
$domain = $this->getTestDomain('kolab.org');
// Ned is an additional controller on kolab.org's wallet
$response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
// Jack has no entitlement/control over kolab.org
$response = $this->actingAs($jack)->get("api/v4/domains/{$domain->id}");
$response->assertStatus(403);
}
/**
* Test fetching SKUs list for a domain (GET /domains/<id>/skus)
*/
public function testSkus(): void
{
$user = $this->getTestUser('john@kolab.org');
$domain = $this->getTestDomain('kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(401);
// Create an sku for another tenant, to make sure it is not included in the result
$nsku = Sku::create([
'title' => 'test',
'name' => 'Test',
'description' => '',
'active' => true,
'cost' => 100,
'handler_class' => 'App\Handlers\Domain',
]);
$tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first();
$nsku->tenant_id = $tenant->id;
$nsku->save();
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(1, $json);
$this->assertSkuElement('domain-hosting', $json[0], [
'prio' => 0,
'type' => 'domain',
'handler' => 'DomainHosting',
'enabled' => true,
'readonly' => true,
]);
}
/**
* Test fetching domain status (GET /api/v4/domains/<domain-id>/status)
* and forcing setup process update (?refresh=1)
*
* @group dns
*/
public function testStatus(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$domain = $this->getTestDomain('kolab.org');
// Test unauthorized access
$response = $this->actingAs($jack)->get("/api/v4/domains/{$domain->id}/status");
$response->assertStatus(403);
$domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY;
$domain->save();
// Get domain status
$response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status");
$response->assertStatus(200);
$json = $response->json();
$this->assertFalse($json['isVerified']);
$this->assertFalse($json['isReady']);
$this->assertCount(4, $json['process']);
$this->assertSame('domain-verified', $json['process'][2]['label']);
$this->assertSame(false, $json['process'][2]['state']);
$this->assertTrue(empty($json['status']));
$this->assertTrue(empty($json['message']));
// Now "reboot" the process and verify the domain
$response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status?refresh=1");
$response->assertStatus(200);
$json = $response->json();
$this->assertTrue($json['isVerified']);
$this->assertTrue($json['isReady']);
$this->assertCount(4, $json['process']);
$this->assertSame('domain-verified', $json['process'][2]['label']);
$this->assertSame(true, $json['process'][2]['state']);
$this->assertSame('domain-confirmed', $json['process'][3]['label']);
$this->assertSame(true, $json['process'][3]['state']);
$this->assertSame('success', $json['status']);
$this->assertSame('Setup process finished successfully.', $json['message']);
// TODO: Test completing all process steps
}
/**
* Test domain creation (POST /api/v4/domains)
*/
public function testStore(): void
{
Queue::fake();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
// Test empty request
$response = $this->actingAs($john)->post("/api/v4/domains", []);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("The namespace field is required.", $json['errors']['namespace'][0]);
$this->assertCount(1, $json['errors']);
$this->assertCount(1, $json['errors']['namespace']);
$this->assertCount(2, $json);
// Test access by user not being a wallet controller
$post = ['namespace' => 'domainscontroller.com'];
$response = $this->actingAs($jack)->post("/api/v4/domains", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['namespace' => '--'];
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame('The specified domain is invalid.', $json['errors']['namespace'][0]);
$this->assertCount(1, $json['errors']);
$this->assertCount(1, $json['errors']['namespace']);
// Test an existing domain
$post = ['namespace' => 'kolab.org'];
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame('The specified domain is not available.', $json['errors']['namespace']);
$package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first();
$package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
// Missing package
$post = ['namespace' => 'domainscontroller.com'];
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertSame("Package is required.", $json['errors']['package']);
$this->assertCount(2, $json);
// Invalid package
$post['package'] = $package_kolab->id;
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertSame("Invalid package selected.", $json['errors']['package']);
$this->assertCount(2, $json);
// Test full and valid data
$post['package'] = $package_domain->id;
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("Domain created successfully.", $json['message']);
$this->assertCount(2, $json);
$domain = Domain::where('namespace', $post['namespace'])->first();
$this->assertInstanceOf(Domain::class, $domain);
// Assert the new domain entitlements
$this->assertEntitlements($domain, ['domain-hosting']);
// Assert the wallet to which the new domain should be assigned to
$wallet = $domain->wallet();
$this->assertSame($john->wallets->first()->id, $wallet->id);
// Test re-creating a domain
$domain->delete();
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("Domain created successfully.", $json['message']);
$this->assertCount(2, $json);
$domain = Domain::where('namespace', $post['namespace'])->first();
$this->assertInstanceOf(Domain::class, $domain);
$this->assertEntitlements($domain, ['domain-hosting']);
$wallet = $domain->wallet();
$this->assertSame($john->wallets->first()->id, $wallet->id);
// Test creating a domain that is soft-deleted and belongs to another user
$domain->delete();
$domain->entitlements()->withTrashed()->update(['wallet_id' => $jack->wallets->first()->id]);
$response = $this->actingAs($john)->post("/api/v4/domains", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame('The specified domain is not available.', $json['errors']['namespace']);
// Test acting as account controller (not owner)
$this->markTestIncomplete();
}
}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
index 5c02132f..9e306d4b 100644
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -1,1656 +1,1669 @@
<?php
namespace Tests\Feature\Controller;
use App\Discount;
use App\Domain;
use App\Http\Controllers\API\V4\UsersController;
use App\Package;
use App\Sku;
use App\Tenant;
use App\User;
use App\Wallet;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
class UsersTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->clearBetaEntitlements();
$this->deleteTestUser('jane@kolabnow.com');
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestUser('UsersControllerTest2@userscontroller.com');
$this->deleteTestUser('UsersControllerTest3@userscontroller.com');
$this->deleteTestUser('UserEntitlement2A@UserEntitlement.com');
$this->deleteTestUser('john2.doe2@kolab.org');
$this->deleteTestUser('deleted@kolab.org');
$this->deleteTestUser('deleted@kolabnow.com');
$this->deleteTestDomain('userscontroller.com');
$this->deleteTestGroup('group-test@kolabnow.com');
$this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestSharedFolder('folder-test@kolabnow.com');
$this->deleteTestResource('resource-test@kolabnow.com');
Sku::where('title', 'test')->delete();
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
$wallet->discount()->dissociate();
$wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete();
$wallet->save();
$user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete();
$user->status |= User::STATUS_IMAP_READY;
$user->save();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->clearBetaEntitlements();
$this->deleteTestUser('jane@kolabnow.com');
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestUser('UsersControllerTest2@userscontroller.com');
$this->deleteTestUser('UsersControllerTest3@userscontroller.com');
$this->deleteTestUser('UserEntitlement2A@UserEntitlement.com');
$this->deleteTestUser('john2.doe2@kolab.org');
$this->deleteTestUser('deleted@kolab.org');
$this->deleteTestUser('deleted@kolabnow.com');
$this->deleteTestDomain('userscontroller.com');
$this->deleteTestGroup('group-test@kolabnow.com');
$this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestSharedFolder('folder-test@kolabnow.com');
$this->deleteTestResource('resource-test@kolabnow.com');
Sku::where('title', 'test')->delete();
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
$wallet->discount()->dissociate();
$wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete();
$wallet->save();
$user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete();
$user->status |= User::STATUS_IMAP_READY;
$user->save();
parent::tearDown();
}
/**
* Test user deleting (DELETE /api/v4/users/<id>)
*/
public function testDestroy(): void
{
// First create some users/accounts to delete
$package_kolab = \App\Package::where('title', 'kolab')->first();
$package_domain = \App\Package::where('title', 'domain-hosting')->first();
$john = $this->getTestUser('john@kolab.org');
$user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com');
$user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com');
$domain = $this->getTestDomain('userscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_PUBLIC,
]);
$user1->assignPackage($package_kolab);
$domain->assignPackage($package_domain, $user1);
$user1->assignPackage($package_kolab, $user2);
$user1->assignPackage($package_kolab, $user3);
// Test unauth access
$response = $this->delete("api/v4/users/{$user2->id}");
$response->assertStatus(401);
// Test access to other user/account
$response = $this->actingAs($john)->delete("api/v4/users/{$user2->id}");
$response->assertStatus(403);
$response = $this->actingAs($john)->delete("api/v4/users/{$user1->id}");
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test that non-controller cannot remove himself
$response = $this->actingAs($user3)->delete("api/v4/users/{$user3->id}");
$response->assertStatus(403);
// Test removing a non-controller user
$response = $this->actingAs($user1)->delete("api/v4/users/{$user3->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals('success', $json['status']);
$this->assertEquals('User deleted successfully.', $json['message']);
// Test removing self (an account with users)
$response = $this->actingAs($user1)->delete("api/v4/users/{$user1->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals('success', $json['status']);
$this->assertEquals('User deleted successfully.', $json['message']);
}
/**
* Test user deleting (DELETE /api/v4/users/<id>)
*/
public function testDestroyByController(): void
{
// Create an account with additional controller - $user2
$package_kolab = \App\Package::where('title', 'kolab')->first();
$package_domain = \App\Package::where('title', 'domain-hosting')->first();
$user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com');
$user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com');
$domain = $this->getTestDomain('userscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_PUBLIC,
]);
$user1->assignPackage($package_kolab);
$domain->assignPackage($package_domain, $user1);
$user1->assignPackage($package_kolab, $user2);
$user1->assignPackage($package_kolab, $user3);
$user1->wallets()->first()->addController($user2);
// TODO/FIXME:
// For now controller can delete himself, as well as
// the whole account he has control to, including the owner
// Probably he should not be able to do none of those
// However, this is not 0-regression scenario as we
// do not fully support additional controllers.
//$response = $this->actingAs($user2)->delete("api/v4/users/{$user2->id}");
//$response->assertStatus(403);
$response = $this->actingAs($user2)->delete("api/v4/users/{$user3->id}");
$response->assertStatus(200);
$response = $this->actingAs($user2)->delete("api/v4/users/{$user1->id}");
$response->assertStatus(200);
// Note: More detailed assertions in testDestroy() above
$this->assertTrue($user1->fresh()->trashed());
$this->assertTrue($user2->fresh()->trashed());
$this->assertTrue($user3->fresh()->trashed());
}
/**
* Test user listing (GET /api/v4/users)
*/
public function testIndex(): void
{
// Test unauth access
$response = $this->get("api/v4/users");
$response->assertStatus(401);
$jack = $this->getTestUser('jack@kolab.org');
$joe = $this->getTestUser('joe@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$response = $this->actingAs($jack)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(false, $json['hasMore']);
$this->assertSame(0, $json['count']);
$this->assertCount(0, $json['list']);
$response = $this->actingAs($john)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(false, $json['hasMore']);
$this->assertSame(4, $json['count']);
$this->assertCount(4, $json['list']);
$this->assertSame($jack->email, $json['list'][0]['email']);
$this->assertSame($joe->email, $json['list'][1]['email']);
$this->assertSame($john->email, $json['list'][2]['email']);
$this->assertSame($ned->email, $json['list'][3]['email']);
// Values below are tested by Unit tests
$this->assertArrayHasKey('isDeleted', $json['list'][0]);
$this->assertArrayHasKey('isDegraded', $json['list'][0]);
$this->assertArrayHasKey('isAccountDegraded', $json['list'][0]);
$this->assertArrayHasKey('isSuspended', $json['list'][0]);
$this->assertArrayHasKey('isActive', $json['list'][0]);
- $this->assertArrayHasKey('isLdapReady', $json['list'][0]);
- $this->assertArrayHasKey('isImapReady', $json['list'][0]);
+ $this->assertArrayHasKey('isReady', $json['list'][0]);
+ if (\config('app.with_ldap')) {
+ $this->assertArrayHasKey('isLdapReady', $json['list'][0]);
+ }
+ if (\config('app.with_imap')) {
+ $this->assertArrayHasKey('isImapReady', $json['list'][0]);
+ }
$response = $this->actingAs($ned)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(false, $json['hasMore']);
$this->assertSame(4, $json['count']);
$this->assertCount(4, $json['list']);
$this->assertSame($jack->email, $json['list'][0]['email']);
$this->assertSame($joe->email, $json['list'][1]['email']);
$this->assertSame($john->email, $json['list'][2]['email']);
$this->assertSame($ned->email, $json['list'][3]['email']);
// Search by user email
$response = $this->actingAs($john)->get("/api/v4/users?search=jack@k");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(false, $json['hasMore']);
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($jack->email, $json['list'][0]['email']);
// Search by alias
$response = $this->actingAs($john)->get("/api/v4/users?search=monster");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(false, $json['hasMore']);
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($joe->email, $json['list'][0]['email']);
// Search by name
$response = $this->actingAs($john)->get("/api/v4/users?search=land");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(false, $json['hasMore']);
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($ned->email, $json['list'][0]['email']);
// TODO: Test paging
}
/**
* Test fetching user data/profile (GET /api/v4/users/<user-id>)
*/
public function testShow(): void
{
$userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com');
// Test getting profile of self
$response = $this->actingAs($userA)->get("/api/v4/users/{$userA->id}");
$json = $response->json();
$response->assertStatus(200);
$this->assertEquals($userA->id, $json['id']);
$this->assertEquals($userA->email, $json['email']);
$this->assertTrue(is_array($json['statusInfo']));
$this->assertTrue(is_array($json['settings']));
$this->assertTrue($json['config']['greylist_enabled']);
$this->assertFalse($json['config']['guam_enabled']);
$this->assertSame([], $json['skus']);
$this->assertSame([], $json['aliases']);
// Values below are tested by Unit tests
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isDegraded', $json);
$this->assertArrayHasKey('isAccountDegraded', $json);
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
- $this->assertArrayHasKey('isLdapReady', $json);
- $this->assertArrayHasKey('isImapReady', $json);
+ $this->assertArrayHasKey('isReady', $json);
+ if (\config('app.with_ldap')) {
+ $this->assertArrayHasKey('isLdapReady', $json);
+ }
+ if (\config('app.with_imap')) {
+ $this->assertArrayHasKey('isImapReady', $json);
+ }
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
// Test unauthorized access to a profile of other user
$response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}");
$response->assertStatus(403);
// Test authorized access to a profile of other user
// Ned: Additional account controller
$response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(['john.doe@kolab.org'], $json['aliases']);
$response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}");
$response->assertStatus(200);
// John: Account owner
$response = $this->actingAs($john)->get("/api/v4/users/{$jack->id}");
$response->assertStatus(200);
$response = $this->actingAs($john)->get("/api/v4/users/{$ned->id}");
$response->assertStatus(200);
$json = $response->json();
$storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
$groupware_sku = Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$mailbox_sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
$secondfactor_sku = Sku::withEnvTenantContext()->where('title', '2fa')->first();
$this->assertCount(5, $json['skus']);
$this->assertSame(5, $json['skus'][$storage_sku->id]['count']);
$this->assertSame([0,0,0,0,0], $json['skus'][$storage_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$groupware_sku->id]['count']);
$this->assertSame([490], $json['skus'][$groupware_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$mailbox_sku->id]['count']);
$this->assertSame([500], $json['skus'][$mailbox_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']);
$this->assertSame([0], $json['skus'][$secondfactor_sku->id]['costs']);
$this->assertSame([], $json['aliases']);
}
/**
* Test fetching SKUs list for a user (GET /users/<id>/skus)
*/
public function testSkus(): void
{
$user = $this->getTestUser('john@kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(401);
// Create an sku for another tenant, to make sure it is not included in the result
$nsku = Sku::create([
'title' => 'test',
'name' => 'Test',
'description' => '',
'active' => true,
'cost' => 100,
'handler_class' => 'Mailbox',
]);
$tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first();
$nsku->tenant_id = $tenant->id;
$nsku->save();
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(5, $json);
$this->assertSkuElement('mailbox', $json[0], [
'prio' => 100,
'type' => 'user',
'handler' => 'Mailbox',
'enabled' => true,
'readonly' => true,
]);
$this->assertSkuElement('storage', $json[1], [
'prio' => 90,
'type' => 'user',
'handler' => 'Storage',
'enabled' => true,
'readonly' => true,
'range' => [
'min' => 5,
'max' => 100,
'unit' => 'GB',
]
]);
$this->assertSkuElement('groupware', $json[2], [
'prio' => 80,
'type' => 'user',
'handler' => 'Groupware',
'enabled' => false,
'readonly' => false,
]);
$this->assertSkuElement('activesync', $json[3], [
'prio' => 70,
'type' => 'user',
'handler' => 'Activesync',
'enabled' => false,
'readonly' => false,
'required' => ['Groupware'],
]);
$this->assertSkuElement('2fa', $json[4], [
'prio' => 60,
'type' => 'user',
'handler' => 'Auth2F',
'enabled' => false,
'readonly' => false,
'forbidden' => ['Activesync'],
]);
// Test inclusion of beta SKUs
$sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$user->assignSku($sku);
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(6, $json);
$this->assertSkuElement('beta', $json[5], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta',
'enabled' => false,
'readonly' => false,
]);
}
/**
* Test fetching user status (GET /api/v4/users/<user-id>/status)
* and forcing setup process update (?refresh=1)
*
* @group imap
* @group dns
*/
public function testStatus(): void
{
Queue::fake();
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
// Test unauthorized access
$response = $this->actingAs($jack)->get("/api/v4/users/{$john->id}/status");
$response->assertStatus(403);
- if ($john->isImapReady()) {
- $john->status ^= User::STATUS_IMAP_READY;
- $john->save();
- }
+ $john->status &= ~User::STATUS_IMAP_READY;
+ $john->status &= ~User::STATUS_LDAP_READY;
+ $john->save();
// Get user status
$response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status");
$response->assertStatus(200);
$json = $response->json();
- $this->assertFalse($json['isImapReady']);
$this->assertFalse($json['isReady']);
+
+ if (\config('app.with_ldap')) {
+ $this->assertFalse($json['isLdapReady']);
+ } else {
+ $this->assertArrayNotHasKey('isLdapReady', $json);
+ }
+
if (\config('app.with_imap')) {
- $this->assertCount(6, $json['process']);
+ $this->assertFalse($json['isImapReady']);
$this->assertSame('user-imap-ready', $json['process'][2]['label']);
- $this->assertSame(false, $json['process'][2]['state']);
+ $this->assertFalse($json['process'][2]['state']);
} else {
- $this->assertCount(7, $json['process']);
+ $this->assertArrayNotHasKey('isImapReady', $json);
}
$this->assertTrue(empty($json['status']));
$this->assertTrue(empty($json['message']));
// Make sure the domain is confirmed (other test might unset that status)
$domain = $this->getTestDomain('kolab.org');
$domain->status |= Domain::STATUS_CONFIRMED;
$domain->save();
// Now "reboot" the process
Queue::fake();
$response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1");
$response->assertStatus(200);
$json = $response->json();
$this->assertFalse($json['isImapReady']);
+ $this->assertFalse($json['isLdapReady']);
$this->assertFalse($json['isReady']);
if (\config('app.with_imap')) {
- $this->assertCount(7, $json['process']);
$this->assertSame('user-imap-ready', $json['process'][2]['label']);
$this->assertSame(false, $json['process'][2]['state']);
- } else {
- $this->assertCount(6, $json['process']);
}
$this->assertSame('success', $json['status']);
$this->assertSame('Setup process has been pushed. Please wait.', $json['message']);
Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1);
}
/**
* Test UsersController::statusInfo()
*/
public function testStatusInfo(): void
{
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$domain = $this->getTestDomain('userscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_PUBLIC,
]);
$user->created_at = Carbon::now();
$user->status = User::STATUS_NEW;
$user->save();
$result = UsersController::statusInfo($user);
$this->assertFalse($result['isReady']);
$this->assertSame([], $result['skus']);
if (\config('app.with_imap')) {
$this->assertCount(3, $result['process']);
} else {
$this->assertCount(2, $result['process']);
}
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
$this->assertSame('user-ldap-ready', $result['process'][1]['label']);
$this->assertSame(false, $result['process'][1]['state']);
if (\config('app.with_imap')) {
$this->assertSame('user-imap-ready', $result['process'][2]['label']);
$this->assertSame(false, $result['process'][2]['state']);
}
$this->assertSame('running', $result['processState']);
$this->assertTrue($result['enableRooms']);
$this->assertFalse($result['enableBeta']);
$user->created_at = Carbon::now()->subSeconds(181);
$user->save();
$result = UsersController::statusInfo($user);
$this->assertSame('failed', $result['processState']);
$user->status |= User::STATUS_LDAP_READY | User::STATUS_IMAP_READY;
$user->save();
$result = UsersController::statusInfo($user);
$this->assertTrue($result['isReady']);
if (\config('app.with_imap')) {
$this->assertCount(3, $result['process']);
} else {
$this->assertCount(2, $result['process']);
}
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
$this->assertSame('user-ldap-ready', $result['process'][1]['label']);
$this->assertSame(true, $result['process'][1]['state']);
if (\config('app.with_imap')) {
$this->assertSame('user-imap-ready', $result['process'][2]['label']);
$this->assertSame(true, $result['process'][2]['state']);
}
$this->assertSame('done', $result['processState']);
$domain->status |= Domain::STATUS_VERIFIED;
$domain->type = Domain::TYPE_EXTERNAL;
$domain->save();
$result = UsersController::statusInfo($user);
$this->assertFalse($result['isReady']);
$this->assertSame([], $result['skus']);
if (\config('app.with_imap')) {
$this->assertCount(7, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
$this->assertSame('user-ldap-ready', $result['process'][1]['label']);
$this->assertSame(true, $result['process'][1]['state']);
$this->assertSame('user-imap-ready', $result['process'][2]['label']);
$this->assertSame(true, $result['process'][2]['state']);
$this->assertSame('domain-new', $result['process'][3]['label']);
$this->assertSame(true, $result['process'][3]['state']);
$this->assertSame('domain-ldap-ready', $result['process'][4]['label']);
$this->assertSame(false, $result['process'][4]['state']);
$this->assertSame('domain-verified', $result['process'][5]['label']);
$this->assertSame(true, $result['process'][5]['state']);
$this->assertSame('domain-confirmed', $result['process'][6]['label']);
$this->assertSame(false, $result['process'][6]['state']);
} else {
$this->assertCount(6, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
$this->assertSame('user-ldap-ready', $result['process'][1]['label']);
$this->assertSame(true, $result['process'][1]['state']);
$this->assertSame('domain-new', $result['process'][2]['label']);
$this->assertSame(true, $result['process'][2]['state']);
$this->assertSame('domain-ldap-ready', $result['process'][3]['label']);
$this->assertSame(false, $result['process'][3]['state']);
$this->assertSame('domain-verified', $result['process'][4]['label']);
$this->assertSame(true, $result['process'][4]['state']);
$this->assertSame('domain-confirmed', $result['process'][5]['label']);
$this->assertSame(false, $result['process'][5]['state']);
}
// Test 'skus' property
$user->assignSku(Sku::withEnvTenantContext()->where('title', 'beta')->first());
$result = UsersController::statusInfo($user);
$this->assertSame(['beta'], $result['skus']);
$this->assertTrue($result['enableBeta']);
$user->assignSku(Sku::withEnvTenantContext()->where('title', 'groupware')->first());
$result = UsersController::statusInfo($user);
$this->assertSame(['beta', 'groupware'], $result['skus']);
// Degraded user
$user->status |= User::STATUS_DEGRADED;
$user->save();
$result = UsersController::statusInfo($user);
$this->assertTrue($result['enableBeta']);
$this->assertFalse($result['enableRooms']);
// User in a tenant without 'room' SKU
$user->status = User::STATUS_LDAP_READY | User::STATUS_IMAP_READY | User::STATUS_ACTIVE;
$user->tenant_id = Tenant::where('title', 'Sample Tenant')->first()->id;
$user->save();
$result = UsersController::statusInfo($user);
$this->assertTrue($result['enableBeta']);
$this->assertFalse($result['enableRooms']);
}
/**
* Test user config update (POST /api/v4/users/<user>/config)
*/
public function testSetConfig(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('greylist_enabled', null);
$john->setSetting('guam_enabled', null);
$john->setSetting('password_policy', null);
$john->setSetting('max_password_age', null);
// Test unknown user id
$post = ['greylist_enabled' => 1];
$response = $this->actingAs($john)->post("/api/v4/users/123/config", $post);
$json = $response->json();
$response->assertStatus(404);
// Test access by user not being a wallet controller
$post = ['greylist_enabled' => 1];
$response = $this->actingAs($jack)->post("/api/v4/users/{$john->id}/config", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['grey' => 1, 'password_policy' => 'min:1,max:255'];
$response = $this->actingAs($john)->post("/api/v4/users/{$john->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(2, $json['errors']);
$this->assertSame("The requested configuration parameter is not supported.", $json['errors']['grey']);
$this->assertSame("Minimum password length cannot be less than 6.", $json['errors']['password_policy']);
$this->assertNull($john->fresh()->getSetting('greylist_enabled'));
// Test some valid data
$post = [
'greylist_enabled' => 1,
'guam_enabled' => 1,
'password_policy' => 'min:10,max:255,upper,lower,digit,special',
'max_password_age' => 6,
];
$response = $this->actingAs($john)->post("/api/v4/users/{$john->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('User settings updated successfully.', $json['message']);
$this->assertSame('true', $john->getSetting('greylist_enabled'));
$this->assertSame('true', $john->getSetting('guam_enabled'));
$this->assertSame('min:10,max:255,upper,lower,digit,special', $john->getSetting('password_policy'));
$this->assertSame('6', $john->getSetting('max_password_age'));
// Test some valid data, acting as another account controller
$ned = $this->getTestUser('ned@kolab.org');
$post = ['greylist_enabled' => 0, 'guam_enabled' => 0, 'password_policy' => 'min:10,max:255,upper,last:1'];
$response = $this->actingAs($ned)->post("/api/v4/users/{$john->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('User settings updated successfully.', $json['message']);
$this->assertSame('false', $john->fresh()->getSetting('greylist_enabled'));
$this->assertSame(null, $john->fresh()->getSetting('guam_enabled'));
$this->assertSame('min:10,max:255,upper,last:1', $john->fresh()->getSetting('password_policy'));
}
/**
* Test user creation (POST /api/v4/users)
*/
public function testStore(): void
{
Queue::fake();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('password_policy', 'min:8,max:100,digit');
$deleted_priv = $this->getTestUser('deleted@kolab.org');
$deleted_priv->delete();
// Test empty request
$response = $this->actingAs($john)->post("/api/v4/users", []);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("The email field is required.", $json['errors']['email']);
$this->assertSame("The password field is required.", $json['errors']['password'][0]);
$this->assertCount(2, $json);
// Test access by user not being a wallet controller
$post = ['first_name' => 'Test'];
$response = $this->actingAs($jack)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['password' => '12345678', 'email' => 'invalid'];
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]);
$this->assertSame('The specified email is invalid.', $json['errors']['email']);
// Test existing user email
$post = [
'password' => 'simple123',
'password_confirmation' => 'simple123',
'first_name' => 'John2',
'last_name' => 'Doe2',
'email' => 'jack.daniels@kolab.org',
];
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame('The specified email is not available.', $json['errors']['email']);
$package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first();
$package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$post = [
'password' => 'simple123',
'password_confirmation' => 'simple123',
'first_name' => 'John2',
'last_name' => 'Doe2',
'email' => 'john2.doe2@kolab.org',
'organization' => 'TestOrg',
'aliases' => ['useralias1@kolab.org', 'deleted@kolab.org'],
];
// Missing package
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertSame("Package is required.", $json['errors']['package']);
$this->assertCount(2, $json);
// Invalid package
$post['package'] = $package_domain->id;
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertSame("Invalid package selected.", $json['errors']['package']);
$this->assertCount(2, $json);
// Test password policy checking
$post['package'] = $package_kolab->id;
$post['password'] = 'password';
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]);
$this->assertSame("Specified password does not comply with the policy.", $json['errors']['password'][1]);
$this->assertCount(2, $json);
// Test password confirmation
$post['password_confirmation'] = 'password';
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertSame("Specified password does not comply with the policy.", $json['errors']['password'][0]);
$this->assertCount(2, $json);
// Test full and valid data
$post['password'] = 'password123';
$post['password_confirmation'] = 'password123';
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("User created successfully.", $json['message']);
$this->assertCount(2, $json);
$user = User::where('email', 'john2.doe2@kolab.org')->first();
$this->assertInstanceOf(User::class, $user);
$this->assertSame('John2', $user->getSetting('first_name'));
$this->assertSame('Doe2', $user->getSetting('last_name'));
$this->assertSame('TestOrg', $user->getSetting('organization'));
/** @var \App\UserAlias[] $aliases */
$aliases = $user->aliases()->orderBy('alias')->get();
$this->assertCount(2, $aliases);
$this->assertSame('deleted@kolab.org', $aliases[0]->alias);
$this->assertSame('useralias1@kolab.org', $aliases[1]->alias);
// Assert the new user entitlements
$this->assertEntitlements($user, ['groupware', 'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage']);
// Assert the wallet to which the new user should be assigned to
$wallet = $user->wallet();
$this->assertSame($john->wallets->first()->id, $wallet->id);
// Attempt to create a user previously deleted
$user->delete();
$post['package'] = $package_kolab->id;
$post['aliases'] = [];
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("User created successfully.", $json['message']);
$this->assertCount(2, $json);
$user = User::where('email', 'john2.doe2@kolab.org')->first();
$this->assertInstanceOf(User::class, $user);
$this->assertSame('John2', $user->getSetting('first_name'));
$this->assertSame('Doe2', $user->getSetting('last_name'));
$this->assertSame('TestOrg', $user->getSetting('organization'));
$this->assertCount(0, $user->aliases()->get());
$this->assertEntitlements($user, ['groupware', 'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage']);
// Test password reset link "mode"
$code = new \App\VerificationCode(['mode' => 'password-reset', 'active' => false]);
$john->verificationcodes()->save($code);
$post = [
'first_name' => 'John2',
'last_name' => 'Doe2',
'email' => 'deleted@kolab.org',
'organization' => '',
'aliases' => [],
'passwordLinkCode' => $code->short_code . '-' . $code->code,
'package' => $package_kolab->id,
];
$response = $this->actingAs($john)->post("/api/v4/users", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("User created successfully.", $json['message']);
$this->assertCount(2, $json);
$user = $this->getTestUser('deleted@kolab.org');
$code->refresh();
$this->assertSame($user->id, $code->user_id);
$this->assertTrue($code->active);
$this->assertTrue(is_string($user->password) && strlen($user->password) >= 60);
// Test acting as account controller not owner, which is not yet supported
$john->wallets->first()->addController($user);
$response = $this->actingAs($user)->post("/api/v4/users", []);
$response->assertStatus(403);
}
/**
* Test user update (PUT /api/v4/users/<user-id>)
*/
public function testUpdate(): void
{
$userA = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$userA->setSetting('password_policy', 'min:8,digit');
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$domain = $this->getTestDomain(
'userscontroller.com',
['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]
);
// Test unauthorized update of other user profile
$response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}", []);
$response->assertStatus(403);
// Test authorized update of account owner by account controller
$response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}", []);
$response->assertStatus(200);
// Test updating of self (empty request)
$response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", []);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertCount(3, $json);
// Test some invalid data
$post = ['password' => '1234567', 'currency' => 'invalid'];
$response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]);
$this->assertSame("Specified password does not comply with the policy.", $json['errors']['password'][1]);
$this->assertSame("The currency must be 3 characters.", $json['errors']['currency'][0]);
// Test full profile update including password
$post = [
'password' => 'simple123',
'password_confirmation' => 'simple123',
'first_name' => 'John2',
'last_name' => 'Doe2',
'organization' => 'TestOrg',
'phone' => '+123 123 123',
'external_email' => 'external@gmail.com',
'billing_address' => 'billing',
'country' => 'CH',
'currency' => 'CHF',
'aliases' => ['useralias1@' . \config('app.domain'), 'useralias2@' . \config('app.domain')]
];
$response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertCount(3, $json);
$this->assertTrue($userA->password != $userA->fresh()->password);
unset($post['password'], $post['password_confirmation'], $post['aliases']);
foreach ($post as $key => $value) {
$this->assertSame($value, $userA->getSetting($key));
}
$aliases = $userA->aliases()->orderBy('alias')->get();
$this->assertCount(2, $aliases);
$this->assertSame('useralias1@' . \config('app.domain'), $aliases[0]->alias);
$this->assertSame('useralias2@' . \config('app.domain'), $aliases[1]->alias);
// Test unsetting values
$post = [
'first_name' => '',
'last_name' => '',
'organization' => '',
'phone' => '',
'external_email' => '',
'billing_address' => '',
'country' => '',
'currency' => '',
'aliases' => ['useralias2@' . \config('app.domain')]
];
$response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertCount(3, $json);
unset($post['aliases']);
foreach ($post as $key => $value) {
$this->assertNull($userA->getSetting($key));
}
$aliases = $userA->aliases()->get();
$this->assertCount(1, $aliases);
$this->assertSame('useralias2@' . \config('app.domain'), $aliases[0]->alias);
// Test error on some invalid aliases missing password confirmation
$post = [
'password' => 'simple123',
'aliases' => [
'useralias2@' . \config('app.domain'),
'useralias1@kolab.org',
'@kolab.org',
]
];
$response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertCount(2, $json['errors']['aliases']);
$this->assertSame("The specified domain is not available.", $json['errors']['aliases'][1]);
$this->assertSame("The specified alias is invalid.", $json['errors']['aliases'][2]);
$this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]);
// Test authorized update of other user
$response = $this->actingAs($ned)->put("/api/v4/users/{$jack->id}", []);
$response->assertStatus(200);
$json = $response->json();
$this->assertTrue(empty($json['statusInfo']));
// TODO: Test error on aliases with invalid/non-existing/other-user's domain
// Create entitlements and additional user for following tests
$owner = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$user = $this->getTestUser('UsersControllerTest2@userscontroller.com');
$package_domain = Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$package_kolab = Package::withEnvTenantContext()->where('title', 'kolab')->first();
$package_lite = Package::withEnvTenantContext()->where('title', 'lite')->first();
$sku_mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
$sku_storage = Sku::withEnvTenantContext()->where('title', 'storage')->first();
$sku_groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$domain = $this->getTestDomain(
'userscontroller.com',
[
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]
);
$domain->assignPackage($package_domain, $owner);
$owner->assignPackage($package_kolab);
$owner->assignPackage($package_lite, $user);
// Non-controller cannot update his own entitlements
$post = ['skus' => []];
$response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
$response->assertStatus(422);
// Test updating entitlements
$post = [
'skus' => [
$sku_mailbox->id => 1,
$sku_storage->id => 6,
$sku_groupware->id => 1,
],
];
$response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post);
$response->assertStatus(200);
$json = $response->json();
$storage_cost = $user->entitlements()
->where('sku_id', $sku_storage->id)
->orderBy('cost')
->pluck('cost')->all();
$this->assertEntitlements(
$user,
['groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage']
);
$this->assertSame([0, 0, 0, 0, 0, 25], $storage_cost);
$this->assertTrue(empty($json['statusInfo']));
// Test password reset link "mode"
$code = new \App\VerificationCode(['mode' => 'password-reset', 'active' => false]);
$owner->verificationcodes()->save($code);
$post = ['passwordLinkCode' => $code->short_code . '-' . $code->code];
$response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$code->refresh();
$this->assertSame($user->id, $code->user_id);
$this->assertTrue($code->active);
$this->assertSame($user->password, $user->fresh()->password);
}
/**
* Test UsersController::updateEntitlements()
*/
public function testUpdateEntitlements(): void
{
$jane = $this->getTestUser('jane@kolabnow.com');
$kolab = Package::withEnvTenantContext()->where('title', 'kolab')->first();
$storage = Sku::withEnvTenantContext()->where('title', 'storage')->first();
$activesync = Sku::withEnvTenantContext()->where('title', 'activesync')->first();
$groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
// standard package, 1 mailbox, 1 groupware, 2 storage
$jane->assignPackage($kolab);
// add 2 storage, 1 activesync
$post = [
'skus' => [
$mailbox->id => 1,
$groupware->id => 1,
$storage->id => 7,
$activesync->id => 1
]
];
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(200);
$this->assertEntitlements(
$jane,
[
'activesync',
'groupware',
'mailbox',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage'
]
);
// add 2 storage, remove 1 activesync
$post = [
'skus' => [
$mailbox->id => 1,
$groupware->id => 1,
$storage->id => 9,
$activesync->id => 0
]
];
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(200);
$this->assertEntitlements(
$jane,
[
'groupware',
'mailbox',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage'
]
);
// add mailbox
$post = [
'skus' => [
$mailbox->id => 2,
$groupware->id => 1,
$storage->id => 9,
$activesync->id => 0
]
];
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(500);
$this->assertEntitlements(
$jane,
[
'groupware',
'mailbox',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage'
]
);
// remove mailbox
$post = [
'skus' => [
$mailbox->id => 0,
$groupware->id => 1,
$storage->id => 9,
$activesync->id => 0
]
];
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(500);
$this->assertEntitlements(
$jane,
[
'groupware',
'mailbox',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage',
'storage'
]
);
// less than free storage
$post = [
'skus' => [
$mailbox->id => 1,
$groupware->id => 1,
$storage->id => 1,
$activesync->id => 0
]
];
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(200);
$this->assertEntitlements(
$jane,
[
'groupware',
'mailbox',
'storage',
'storage',
'storage',
'storage',
'storage'
]
);
}
/**
* Test user data response used in show and info actions
*/
public function testUserResponse(): void
{
$provider = \config('services.payment_provider') ?: 'mollie';
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
$wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]);
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]);
$this->assertEquals($user->id, $result['id']);
$this->assertEquals($user->email, $result['email']);
$this->assertEquals($user->status, $result['status']);
$this->assertTrue(is_array($result['statusInfo']));
$this->assertTrue(is_array($result['settings']));
$this->assertSame('US', $result['settings']['country']);
$this->assertSame('USD', $result['settings']['currency']);
$this->assertTrue(is_array($result['accounts']));
$this->assertTrue(is_array($result['wallets']));
$this->assertCount(0, $result['accounts']);
$this->assertCount(1, $result['wallets']);
$this->assertSame($wallet->id, $result['wallet']['id']);
$this->assertArrayNotHasKey('discount', $result['wallet']);
$this->assertTrue($result['statusInfo']['enableDomains']);
$this->assertTrue($result['statusInfo']['enableWallets']);
$this->assertTrue($result['statusInfo']['enableUsers']);
$this->assertTrue($result['statusInfo']['enableSettings']);
// Ned is John's wallet controller
$ned = $this->getTestUser('ned@kolab.org');
$ned_wallet = $ned->wallets()->first();
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]);
$this->assertEquals($ned->id, $result['id']);
$this->assertEquals($ned->email, $result['email']);
$this->assertTrue(is_array($result['accounts']));
$this->assertTrue(is_array($result['wallets']));
$this->assertCount(1, $result['accounts']);
$this->assertCount(1, $result['wallets']);
$this->assertSame($wallet->id, $result['wallet']['id']);
$this->assertSame($wallet->id, $result['accounts'][0]['id']);
$this->assertSame($ned_wallet->id, $result['wallets'][0]['id']);
$this->assertSame($provider, $result['wallet']['provider']);
$this->assertSame($provider, $result['wallets'][0]['provider']);
$this->assertTrue($result['statusInfo']['enableDomains']);
$this->assertTrue($result['statusInfo']['enableWallets']);
$this->assertTrue($result['statusInfo']['enableUsers']);
$this->assertTrue($result['statusInfo']['enableSettings']);
// Test discount in a response
$discount = Discount::where('code', 'TEST')->first();
$wallet->discount()->associate($discount);
$wallet->save();
$mod_provider = $provider == 'mollie' ? 'stripe' : 'mollie';
$wallet->setSetting($mod_provider . '_id', 123);
$user->refresh();
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]);
$this->assertEquals($user->id, $result['id']);
$this->assertSame($discount->id, $result['wallet']['discount_id']);
$this->assertSame($discount->discount, $result['wallet']['discount']);
$this->assertSame($discount->description, $result['wallet']['discount_description']);
$this->assertSame($mod_provider, $result['wallet']['provider']);
$this->assertSame($discount->id, $result['wallets'][0]['discount_id']);
$this->assertSame($discount->discount, $result['wallets'][0]['discount']);
$this->assertSame($discount->description, $result['wallets'][0]['discount_description']);
$this->assertSame($mod_provider, $result['wallets'][0]['provider']);
// Jack is not a John's wallet controller
$jack = $this->getTestUser('jack@kolab.org');
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$jack]);
$this->assertFalse($result['statusInfo']['enableDomains']);
$this->assertFalse($result['statusInfo']['enableWallets']);
$this->assertFalse($result['statusInfo']['enableUsers']);
$this->assertFalse($result['statusInfo']['enableSettings']);
}
/**
* User email address validation.
*
* Note: Technically these include unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*/
public function testValidateEmail(): void
{
Queue::fake();
$public_domains = Domain::getPublicDomains();
$domain = reset($public_domains);
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$folder = $this->getTestSharedFolder('folder-event@kolab.org');
$folder->setAliases(['folder-alias1@kolab.org']);
$folder_del = $this->getTestSharedFolder('folder-test@kolabnow.com');
$folder_del->setAliases(['folder-alias2@kolabnow.com']);
$folder_del->delete();
$pub_group = $this->getTestGroup('group-test@kolabnow.com');
$pub_group->delete();
$priv_group = $this->getTestGroup('group-test@kolab.org');
$resource = $this->getTestResource('resource-test@kolabnow.com');
$resource->delete();
$cases = [
// valid (user domain)
["admin@kolab.org", $john, null],
// valid (public domain)
["test.test@$domain", $john, null],
// Invalid format
["$domain", $john, 'The specified email is invalid.'],
[".@$domain", $john, 'The specified email is invalid.'],
["test123456@localhost", $john, 'The specified domain is invalid.'],
["test123456@unknown-domain.org", $john, 'The specified domain is invalid.'],
["$domain", $john, 'The specified email is invalid.'],
[".@$domain", $john, 'The specified email is invalid.'],
// forbidden local part on public domains
["admin@$domain", $john, 'The specified email is not available.'],
["administrator@$domain", $john, 'The specified email is not available.'],
// forbidden (other user's domain)
["testtest@kolab.org", $user, 'The specified domain is not available.'],
// existing alias of other user
["jack.daniels@kolab.org", $john, 'The specified email is not available.'],
// An existing shared folder or folder alias
["folder-event@kolab.org", $john, 'The specified email is not available.'],
["folder-alias1@kolab.org", $john, 'The specified email is not available.'],
// A soft-deleted shared folder or folder alias
["folder-test@kolabnow.com", $john, 'The specified email is not available.'],
["folder-alias2@kolabnow.com", $john, 'The specified email is not available.'],
// A group
["group-test@kolab.org", $john, 'The specified email is not available.'],
// A soft-deleted group
["group-test@kolabnow.com", $john, 'The specified email is not available.'],
// A resource
["resource-test1@kolab.org", $john, 'The specified email is not available.'],
// A soft-deleted resource
["resource-test@kolabnow.com", $john, 'The specified email is not available.'],
];
foreach ($cases as $idx => $case) {
list($email, $user, $expected) = $case;
$deleted = null;
$result = UsersController::validateEmail($email, $user, $deleted);
$this->assertSame($expected, $result, "Case {$email}");
$this->assertNull($deleted, "Case {$email}");
}
}
/**
* User email validation - tests for $deleted argument
*
* Note: Technically these include unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*/
public function testValidateEmailDeleted(): void
{
Queue::fake();
$john = $this->getTestUser('john@kolab.org');
$deleted_priv = $this->getTestUser('deleted@kolab.org');
$deleted_priv->delete();
$deleted_pub = $this->getTestUser('deleted@kolabnow.com');
$deleted_pub->delete();
$result = UsersController::validateEmail('deleted@kolab.org', $john, $deleted);
$this->assertSame(null, $result);
$this->assertSame($deleted_priv->id, $deleted->id);
$result = UsersController::validateEmail('deleted@kolabnow.com', $john, $deleted);
$this->assertSame('The specified email is not available.', $result);
$this->assertSame(null, $deleted);
$result = UsersController::validateEmail('jack@kolab.org', $john, $deleted);
$this->assertSame('The specified email is not available.', $result);
$this->assertSame(null, $deleted);
$pub_group = $this->getTestGroup('group-test@kolabnow.com');
$priv_group = $this->getTestGroup('group-test@kolab.org');
// A group in a public domain, existing
$result = UsersController::validateEmail($pub_group->email, $john, $deleted);
$this->assertSame('The specified email is not available.', $result);
$this->assertNull($deleted);
$pub_group->delete();
// A group in a public domain, deleted
$result = UsersController::validateEmail($pub_group->email, $john, $deleted);
$this->assertSame('The specified email is not available.', $result);
$this->assertNull($deleted);
// A group in a private domain, existing
$result = UsersController::validateEmail($priv_group->email, $john, $deleted);
$this->assertSame('The specified email is not available.', $result);
$this->assertNull($deleted);
$priv_group->delete();
// A group in a private domain, deleted
$result = UsersController::validateEmail($priv_group->email, $john, $deleted);
$this->assertSame(null, $result);
$this->assertSame($priv_group->id, $deleted->id);
// TODO: Test the same with a resource and shared folder
}
/**
* User email alias validation.
*
* Note: Technically these include unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*/
public function testValidateAlias(): void
{
Queue::fake();
$public_domains = Domain::getPublicDomains();
$domain = reset($public_domains);
$john = $this->getTestUser('john@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$deleted_priv = $this->getTestUser('deleted@kolab.org');
$deleted_priv->setAliases(['deleted-alias@kolab.org']);
$deleted_priv->delete();
$deleted_pub = $this->getTestUser('deleted@kolabnow.com');
$deleted_pub->setAliases(['deleted-alias@kolabnow.com']);
$deleted_pub->delete();
$folder = $this->getTestSharedFolder('folder-event@kolab.org');
$folder->setAliases(['folder-alias1@kolab.org']);
$folder_del = $this->getTestSharedFolder('folder-test@kolabnow.com');
$folder_del->setAliases(['folder-alias2@kolabnow.com']);
$folder_del->delete();
$group_priv = $this->getTestGroup('group-test@kolab.org');
$group = $this->getTestGroup('group-test@kolabnow.com');
$group->delete();
$resource = $this->getTestResource('resource-test@kolabnow.com');
$resource->delete();
$cases = [
// Invalid format
["$domain", $john, 'The specified alias is invalid.'],
[".@$domain", $john, 'The specified alias is invalid.'],
["test123456@localhost", $john, 'The specified domain is invalid.'],
["test123456@unknown-domain.org", $john, 'The specified domain is invalid.'],
["$domain", $john, 'The specified alias is invalid.'],
[".@$domain", $john, 'The specified alias is invalid.'],
// forbidden local part on public domains
["admin@$domain", $john, 'The specified alias is not available.'],
["administrator@$domain", $john, 'The specified alias is not available.'],
// forbidden (other user's domain)
["testtest@kolab.org", $user, 'The specified domain is not available.'],
// existing alias of other user, to be an alias, user in the same group account
["jack.daniels@kolab.org", $john, null],
// existing user
["jack@kolab.org", $john, 'The specified alias is not available.'],
// valid (user domain)
["admin@kolab.org", $john, null],
// valid (public domain)
["test.test@$domain", $john, null],
// An alias that was a user email before is allowed, but only for custom domains
["deleted@kolab.org", $john, null],
["deleted-alias@kolab.org", $john, null],
["deleted@kolabnow.com", $john, 'The specified alias is not available.'],
["deleted-alias@kolabnow.com", $john, 'The specified alias is not available.'],
// An existing shared folder or folder alias
["folder-event@kolab.org", $john, 'The specified alias is not available.'],
["folder-alias1@kolab.org", $john, null],
// A soft-deleted shared folder or folder alias
["folder-test@kolabnow.com", $john, 'The specified alias is not available.'],
["folder-alias2@kolabnow.com", $john, 'The specified alias is not available.'],
// A group with the same email address exists
["group-test@kolab.org", $john, 'The specified alias is not available.'],
// A soft-deleted group
["group-test@kolabnow.com", $john, 'The specified alias is not available.'],
// A resource
["resource-test1@kolab.org", $john, 'The specified alias is not available.'],
// A soft-deleted resource
["resource-test@kolabnow.com", $john, 'The specified alias is not available.'],
];
foreach ($cases as $idx => $case) {
list($alias, $user, $expected) = $case;
$result = UsersController::validateAlias($alias, $user);
$this->assertSame($expected, $result, "Case {$alias}");
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jun 29, 7:11 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
201492
Default Alt Text
(121 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment